Просмотр исходного кода

Horizon: Implement arp:r and arp:w services (#5802)

* Horizon: Implement arp:r and arp:w services

* Fix formatting

* Remove HLE arp services

* Revert "Remove HLE arp services"

This reverts commit c576fcccadb963db56b96bacabd1c1ac7abfb1ab.

* Keep LibHac impl since it's used in bcat

* Addresses gdkchan's feedback

* ArpApi in PrepoIpcServer and remove LmApi

* Fix 2

* Fixes ArpApi init

* Fix encoding

* Update PrepoService.cs

* Fix prepo
Ac_K 2 лет назад
Родитель
Сommit
cd37c75b82
40 измененных файлов с 1415 добавлено и 37 удалено
  1. 11 0
      src/Ryujinx.Common/Memory/StructArrayHelpers.cs
  2. 12 0
      src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs
  3. 0 8
      src/Ryujinx.HLE/HOS/Services/Arp/IReader.cs
  4. 0 8
      src/Ryujinx.HLE/HOS/Services/Arp/IWriter.cs
  5. 0 2
      src/Ryujinx.HLE/HOS/Services/Pctl/ParentalControlServiceFactory/IParentalControlService.cs
  6. 2 1
      src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs
  7. 1 1
      src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
  8. 1 1
      src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
  9. 3 2
      src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
  10. 22 1
      src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
  11. 61 0
      src/Ryujinx.Horizon/Arp/ArpIpcServer.cs
  12. 20 0
      src/Ryujinx.Horizon/Arp/ArpMain.cs
  13. 135 0
      src/Ryujinx.Horizon/Arp/Ipc/Reader.cs
  14. 52 0
      src/Ryujinx.Horizon/Arp/Ipc/Registrar.cs
  15. 25 0
      src/Ryujinx.Horizon/Arp/Ipc/UnregistrationNotifier.cs
  16. 74 0
      src/Ryujinx.Horizon/Arp/Ipc/Updater.cs
  17. 75 0
      src/Ryujinx.Horizon/Arp/Ipc/Writer.cs
  18. 17 4
      src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs
  19. 6 1
      src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs
  20. 11 7
      src/Ryujinx.Horizon/Prepo/PrepoServerManager.cs
  21. 9 0
      src/Ryujinx.Horizon/Sdk/Arp/ApplicationCertificate.cs
  22. 8 0
      src/Ryujinx.Horizon/Sdk/Arp/ApplicationKind.cs
  23. 14 0
      src/Ryujinx.Horizon/Sdk/Arp/ApplicationLaunchProperty.cs
  24. 10 0
      src/Ryujinx.Horizon/Sdk/Arp/ApplicationProcessProperty.cs
  25. 130 0
      src/Ryujinx.Horizon/Sdk/Arp/ArpApi.cs
  26. 17 0
      src/Ryujinx.Horizon/Sdk/Arp/ArpResult.cs
  27. 13 0
      src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstance.cs
  28. 31 0
      src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstanceManager.cs
  29. 18 0
      src/Ryujinx.Horizon/Sdk/Arp/IReader.cs
  30. 12 0
      src/Ryujinx.Horizon/Sdk/Arp/IRegistrar.cs
  31. 9 0
      src/Ryujinx.Horizon/Sdk/Arp/IUnregistrationNotifier.cs
  32. 12 0
      src/Ryujinx.Horizon/Sdk/Arp/IUpdater.cs
  33. 12 0
      src/Ryujinx.Horizon/Sdk/Arp/IWriter.cs
  34. 1 1
      src/Ryujinx.Horizon/Sdk/Ncm/ApplicationId.cs
  35. 13 0
      src/Ryujinx.Horizon/Sdk/Ncm/StorageId.cs
  36. 309 0
      src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs
  37. 249 0
      src/Ryujinx.Horizon/Sdk/ServiceUtil.cs
  38. 7 0
      src/Ryujinx.Horizon/Sdk/Sf/Cmif/CmifRequest.cs
  39. 7 0
      src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcBufferDescriptor.cs
  40. 6 0
      src/Ryujinx.Horizon/ServiceTable.cs

+ 11 - 0
src/Ryujinx.Common/Memory/StructArrayHelpers.cs

@@ -744,6 +744,17 @@ namespace Ryujinx.Common.Memory
         public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
     }
 
+    public struct Array65<T> : IArray<T> where T : unmanaged
+    {
+        T _e0;
+        Array64<T> _other;
+        public readonly int Length => 65;
+        public ref T this[int index] => ref AsSpan()[index];
+
+        [Pure]
+        public Span<T> AsSpan() => MemoryMarshal.CreateSpan(ref _e0, Length);
+    }
+
     public struct Array73<T> : IArray<T> where T : unmanaged
     {
         T _e0;

+ 12 - 0
src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs

@@ -63,6 +63,18 @@ namespace Ryujinx.Common.Memory
         public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref _element, Size);
     }
 
+    [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)]
+    public struct ByteArray3000 : IArray<byte>
+    {
+        private const int Size = 3000;
+
+        byte _element;
+
+        public readonly int Length => Size;
+        public ref byte this[int index] => ref AsSpan()[index];
+        public Span<byte> AsSpan() => MemoryMarshal.CreateSpan(ref _element, Size);
+    }
+
     [StructLayout(LayoutKind.Sequential, Size = Size, Pack = 1)]
     public struct ByteArray4096 : IArray<byte>
     {

+ 0 - 8
src/Ryujinx.HLE/HOS/Services/Arp/IReader.cs

@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Arp
-{
-    [Service("arp:r")]
-    class IReader : IpcService
-    {
-        public IReader(ServiceCtx context) { }
-    }
-}

+ 0 - 8
src/Ryujinx.HLE/HOS/Services/Arp/IWriter.cs

@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Arp
-{
-    [Service("arp:w")]
-    class IWriter : IpcService
-    {
-        public IWriter(ServiceCtx context) { }
-    }
-}

+ 0 - 2
src/Ryujinx.HLE/HOS/Services/Pctl/ParentalControlServiceFactory/IParentalControlService.cs

@@ -159,9 +159,7 @@ namespace Ryujinx.HLE.HOS.Services.Pctl.ParentalControlServiceFactory
             }
             else
             {
-#pragma warning disable CS0162 // Unreachable code
                 return ResultCode.StereoVisionRestrictionConfigurableDisabled;
-#pragma warning restore CS0162
             }
         }
 

+ 2 - 1
src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs

@@ -34,7 +34,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
             return metaLoader;
         }
 
-        public static ProcessResult Load(this IFileSystem exeFs, Switch device, BlitStruct<ApplicationControlProperty> nacpData, MetaLoader metaLoader, bool isHomebrew = false)
+        public static ProcessResult Load(this IFileSystem exeFs, Switch device, BlitStruct<ApplicationControlProperty> nacpData, MetaLoader metaLoader, byte programIndex, bool isHomebrew = false)
         {
             ulong programId = metaLoader.GetProgramId();
 
@@ -119,6 +119,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
                 true,
                 programName,
                 metaLoader.GetProgramId(),
+                programIndex,
                 null,
                 nsoExecutables);
 

+ 1 - 1
src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs

@@ -26,7 +26,7 @@ namespace Ryujinx.HLE.Loaders.Processes
                 ProcessLoaderHelper.EnsureSaveData(device, new ApplicationId(programId), nacpData);
             }
 
-            ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader);
+            ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader, 0);
 
             // Load RomFS.
             if (!string.IsNullOrEmpty(romFsPath))

+ 1 - 1
src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs

@@ -61,7 +61,7 @@ namespace Ryujinx.HLE.Loaders.Processes.Extensions
 
             */
 
-            ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader);
+            ProcessResult processResult = exeFs.Load(device, nacpData, metaLoader, (byte)nca.GetProgramIndex());
 
             // Load RomFS.
             if (romFs == null)

+ 3 - 2
src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs

@@ -77,7 +77,7 @@ namespace Ryujinx.HLE.Loaders.Processes
             if (processResult.ProcessId == 0)
             {
                 // This is not a normal NSP, it's actually a ExeFS as a NSP
-                processResult = partitionFileSystem.Load(_device, new BlitStruct<ApplicationControlProperty>(1), partitionFileSystem.GetNpdm(), true);
+                processResult = partitionFileSystem.Load(_device, new BlitStruct<ApplicationControlProperty>(1), partitionFileSystem.GetNpdm(), 0, true);
             }
 
             if (processResult.ProcessId != 0 && _processesByPid.TryAdd(processResult.ProcessId, processResult))
@@ -198,7 +198,7 @@ namespace Ryujinx.HLE.Loaders.Processes
             }
             else
             {
-                programName = System.IO.Path.GetFileNameWithoutExtension(path);
+                programName = Path.GetFileNameWithoutExtension(path);
 
                 executable = new NsoExecutable(new LocalStorage(path, FileAccess.Read), programName);
             }
@@ -215,6 +215,7 @@ namespace Ryujinx.HLE.Loaders.Processes
                                                                        allowCodeMemoryForJit: true,
                                                                        programName,
                                                                        programId,
+                                                                       0,
                                                                        null,
                                                                        executable);
 

+ 22 - 1
src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs

@@ -19,6 +19,7 @@ using Ryujinx.HLE.HOS.Kernel.Process;
 using Ryujinx.HLE.Loaders.Executables;
 using Ryujinx.HLE.Loaders.Processes.Extensions;
 using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Arp;
 using System;
 using System.Linq;
 using System.Runtime.InteropServices;
@@ -229,6 +230,7 @@ namespace Ryujinx.HLE.Loaders.Processes
             bool allowCodeMemoryForJit,
             string name,
             ulong programId,
+            byte programIndex,
             byte[] arguments = null,
             params IExecutable[] executables)
         {
@@ -421,7 +423,7 @@ namespace Ryujinx.HLE.Loaders.Processes
             // Once everything is loaded, we can load cheats.
             device.Configuration.VirtualFileSystem.ModLoader.LoadCheats(programId, tamperInfo, device.TamperMachine);
 
-            return new ProcessResult(
+            ProcessResult processResult = new(
                 metaLoader,
                 applicationControlProperties,
                 diskCacheEnabled,
@@ -431,6 +433,25 @@ namespace Ryujinx.HLE.Loaders.Processes
                 meta.MainThreadPriority,
                 meta.MainThreadStackSize,
                 device.System.State.DesiredTitleLanguage);
+
+            // Register everything in arp service.
+            device.System.ServiceTable.ArpWriter.AcquireRegistrar(out IRegistrar registrar);
+            registrar.SetApplicationControlProperty(MemoryMarshal.Cast<byte, Horizon.Sdk.Ns.ApplicationControlProperty>(applicationControlProperties.ByteSpan)[0]);
+            // TODO: Handle Version and StorageId when it will be needed.
+            registrar.SetApplicationLaunchProperty(new ApplicationLaunchProperty()
+            {
+                ApplicationId = new Horizon.Sdk.Ncm.ApplicationId(programId),
+                Version = 0x00,
+                Storage = Horizon.Sdk.Ncm.StorageId.BuiltInUser,
+                PatchStorage = Horizon.Sdk.Ncm.StorageId.None,
+                ApplicationKind = ApplicationKind.Application,
+            });
+
+            device.System.ServiceTable.ArpReader.GetApplicationInstanceId(out ulong applicationInstanceId, process.Pid);
+            device.System.ServiceTable.ArpWriter.AcquireApplicationProcessPropertyUpdater(out IUpdater updater, applicationInstanceId);
+            updater.SetApplicationProcessProperty(process.Pid, new ApplicationProcessProperty() { ProgramIndex = programIndex });
+
+            return processResult;
         }
 
         public static Result LoadIntoMemory(KProcess process, IExecutable image, ulong baseAddress)

+ 61 - 0
src/Ryujinx.Horizon/Arp/ArpIpcServer.cs

@@ -0,0 +1,61 @@
+using Ryujinx.Horizon.Arp.Ipc;
+using Ryujinx.Horizon.Sdk.Arp;
+using Ryujinx.Horizon.Sdk.Arp.Detail;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+
+namespace Ryujinx.Horizon.Arp
+{
+    class ArpIpcServer
+    {
+        private const int ArpRMaxSessionsCount = 16;
+        private const int ArpWMaxSessionsCount = 8;
+        private const int MaxSessionsCount = ArpRMaxSessionsCount + ArpWMaxSessionsCount;
+
+        private const int PointerBufferSize = 0x1000;
+        private const int MaxDomains = 24;
+        private const int MaxDomainObjects = 32;
+        private const int MaxPortsCount = 2;
+
+        private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
+
+        private SmApi _sm;
+        private ServerManager _serverManager;
+        private ApplicationInstanceManager _applicationInstanceManager;
+
+        public IReader Reader { get; private set; }
+        public IWriter Writer { get; private set; }
+
+        public void Initialize()
+        {
+            HeapAllocator allocator = new();
+
+            _sm = new SmApi();
+            _sm.Initialize().AbortOnFailure();
+
+            _serverManager = new ServerManager(allocator, _sm, MaxPortsCount, _managerOptions, MaxSessionsCount);
+
+            _applicationInstanceManager = new ApplicationInstanceManager();
+
+            Reader reader = new(_applicationInstanceManager);
+            Reader = reader;
+
+            Writer writer = new(_applicationInstanceManager);
+            Writer = writer;
+
+            _serverManager.RegisterObjectForServer(reader, ServiceName.Encode("arp:r"), ArpRMaxSessionsCount);
+            _serverManager.RegisterObjectForServer(writer, ServiceName.Encode("arp:w"), ArpWMaxSessionsCount);
+        }
+
+        public void ServiceRequests()
+        {
+            _serverManager.ServiceRequests();
+        }
+
+        public void Shutdown()
+        {
+            _applicationInstanceManager.Dispose();
+            _serverManager.Dispose();
+        }
+    }
+}

+ 20 - 0
src/Ryujinx.Horizon/Arp/ArpMain.cs

@@ -0,0 +1,20 @@
+namespace Ryujinx.Horizon.Arp
+{
+    class ArpMain : IService
+    {
+        public static void Main(ServiceTable serviceTable)
+        {
+            ArpIpcServer arpIpcServer = new();
+
+            arpIpcServer.Initialize();
+
+            serviceTable.ArpReader = arpIpcServer.Reader;
+            serviceTable.ArpWriter = arpIpcServer.Writer;
+
+            serviceTable.SignalServiceReady();
+
+            arpIpcServer.ServiceRequests();
+            arpIpcServer.Shutdown();
+        }
+    }
+}

+ 135 - 0
src/Ryujinx.Horizon/Arp/Ipc/Reader.cs

@@ -0,0 +1,135 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Arp;
+using Ryujinx.Horizon.Sdk.Arp.Detail;
+using Ryujinx.Horizon.Sdk.Ns;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+
+namespace Ryujinx.Horizon.Arp.Ipc
+{
+    partial class Reader : IReader, IServiceObject
+    {
+        private readonly ApplicationInstanceManager _applicationInstanceManager;
+
+        public Reader(ApplicationInstanceManager applicationInstanceManager)
+        {
+            _applicationInstanceManager = applicationInstanceManager;
+        }
+
+        [CmifCommand(0)]
+        public Result GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, ulong applicationInstanceId)
+        {
+            if (_applicationInstanceManager.Entries[applicationInstanceId] == null || !_applicationInstanceManager.Entries[applicationInstanceId].LaunchProperty.HasValue)
+            {
+                applicationLaunchProperty = default;
+
+                return ArpResult.InvalidInstanceId;
+            }
+
+            applicationLaunchProperty = _applicationInstanceManager.Entries[applicationInstanceId].LaunchProperty.Value;
+
+            return Result.Success;
+        }
+
+        [CmifCommand(1)]
+        public Result GetApplicationControlProperty([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias, 0x4000)] out ApplicationControlProperty applicationControlProperty, ulong applicationInstanceId)
+        {
+            if (_applicationInstanceManager.Entries[applicationInstanceId] == null || !_applicationInstanceManager.Entries[applicationInstanceId].ControlProperty.HasValue)
+            {
+                applicationControlProperty = default;
+
+                return ArpResult.InvalidInstanceId;
+            }
+
+            applicationControlProperty = _applicationInstanceManager.Entries[applicationInstanceId].ControlProperty.Value;
+
+            return Result.Success;
+        }
+
+        [CmifCommand(2)]
+        public Result GetApplicationProcessProperty(out ApplicationProcessProperty applicationProcessProperty, ulong applicationInstanceId)
+        {
+            if (_applicationInstanceManager.Entries[applicationInstanceId] == null || !_applicationInstanceManager.Entries[applicationInstanceId].ProcessProperty.HasValue)
+            {
+                applicationProcessProperty = default;
+
+                return ArpResult.InvalidInstanceId;
+            }
+
+            applicationProcessProperty = _applicationInstanceManager.Entries[applicationInstanceId].ProcessProperty.Value;
+
+            return Result.Success;
+        }
+
+        [CmifCommand(3)]
+        public Result GetApplicationInstanceId(out ulong applicationInstanceId, ulong pid)
+        {
+            applicationInstanceId = 0;
+
+            if (pid == 0)
+            {
+                return ArpResult.InvalidPid;
+            }
+
+            for (int i = 0; i < _applicationInstanceManager.Entries.Length; i++)
+            {
+                if (_applicationInstanceManager.Entries[i] != null && _applicationInstanceManager.Entries[i].Pid == pid)
+                {
+                    applicationInstanceId = (ulong)i;
+
+                    return Result.Success;
+                }
+            }
+
+            return ArpResult.InvalidPid;
+        }
+
+        [CmifCommand(4)]
+        public Result GetApplicationInstanceUnregistrationNotifier(out IUnregistrationNotifier unregistrationNotifier)
+        {
+            unregistrationNotifier = new UnregistrationNotifier(_applicationInstanceManager);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(5)]
+        public Result ListApplicationInstanceId(out int count, [Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias)] Span<ulong> applicationInstanceIdList)
+        {
+            count = 0;
+
+            if (_applicationInstanceManager.Entries[0] != null)
+            {
+                applicationInstanceIdList[count++] = 0;
+            }
+
+            if (_applicationInstanceManager.Entries[1] != null)
+            {
+                applicationInstanceIdList[count++] = 1;
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(6)]
+        public Result GetMicroApplicationInstanceId(out ulong microApplicationInstanceId, [ClientProcessId] ulong pid)
+        {
+            return GetApplicationInstanceId(out microApplicationInstanceId, pid);
+        }
+
+        [CmifCommand(7)]
+        public Result GetApplicationCertificate([Buffer(HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.FixedSize, 0x528)] out ApplicationCertificate applicationCertificate, ulong applicationInstanceId)
+        {
+            if (_applicationInstanceManager.Entries[applicationInstanceId] == null)
+            {
+                applicationCertificate = default;
+
+                return ArpResult.InvalidInstanceId;
+            }
+
+            applicationCertificate = _applicationInstanceManager.Entries[applicationInstanceId].Certificate.Value;
+
+            return Result.Success;
+        }
+    }
+}

+ 52 - 0
src/Ryujinx.Horizon/Arp/Ipc/Registrar.cs

@@ -0,0 +1,52 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Arp;
+using Ryujinx.Horizon.Sdk.Arp.Detail;
+using Ryujinx.Horizon.Sdk.Ns;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+
+namespace Ryujinx.Horizon.Arp.Ipc
+{
+    partial class Registrar : IRegistrar, IServiceObject
+    {
+        private readonly ApplicationInstance _applicationInstance;
+
+        public Registrar(ApplicationInstance applicationInstance)
+        {
+            _applicationInstance = applicationInstance;
+        }
+
+        [CmifCommand(0)]
+        public Result Issue(out ulong applicationInstanceId)
+        {
+            throw new NotImplementedException();
+        }
+
+        [CmifCommand(1)]
+        public Result SetApplicationLaunchProperty(ApplicationLaunchProperty applicationLaunchProperty)
+        {
+            if (_applicationInstance.LaunchProperty != null)
+            {
+                return ArpResult.DataAlreadyBound;
+            }
+
+            _applicationInstance.LaunchProperty = applicationLaunchProperty;
+
+            return Result.Success;
+        }
+
+        [CmifCommand(2)]
+        public Result SetApplicationControlProperty([Buffer(HipcBufferFlags.In | HipcBufferFlags.MapAlias | HipcBufferFlags.FixedSize, 0x4000)] in ApplicationControlProperty applicationControlProperty)
+        {
+            if (_applicationInstance.ControlProperty != null)
+            {
+                return ArpResult.DataAlreadyBound;
+            }
+
+            _applicationInstance.ControlProperty = applicationControlProperty;
+
+            return Result.Success;
+        }
+    }
+}

+ 25 - 0
src/Ryujinx.Horizon/Arp/Ipc/UnregistrationNotifier.cs

@@ -0,0 +1,25 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Arp;
+using Ryujinx.Horizon.Sdk.Arp.Detail;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Arp.Ipc
+{
+    partial class UnregistrationNotifier : IUnregistrationNotifier, IServiceObject
+    {
+        private readonly ApplicationInstanceManager _applicationInstanceManager;
+
+        public UnregistrationNotifier(ApplicationInstanceManager applicationInstanceManager)
+        {
+            _applicationInstanceManager = applicationInstanceManager;
+        }
+
+        [CmifCommand(0)]
+        public Result GetReadableHandle([CopyHandle] out int readableHandle)
+        {
+            readableHandle = _applicationInstanceManager.EventHandle;
+
+            return Result.Success;
+        }
+    }
+}

+ 74 - 0
src/Ryujinx.Horizon/Arp/Ipc/Updater.cs

@@ -0,0 +1,74 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Arp;
+using Ryujinx.Horizon.Sdk.Arp.Detail;
+using Ryujinx.Horizon.Sdk.Sf;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using System;
+
+namespace Ryujinx.Horizon.Arp.Ipc
+{
+    partial class Updater : IUpdater, IServiceObject
+    {
+        private readonly ApplicationInstanceManager _applicationInstanceManager;
+        private readonly ulong _applicationInstanceId;
+        private readonly bool _forCertificate;
+
+        public Updater(ApplicationInstanceManager applicationInstanceManager, ulong applicationInstanceId, bool forCertificate)
+        {
+            _applicationInstanceManager = applicationInstanceManager;
+            _applicationInstanceId = applicationInstanceId;
+            _forCertificate = forCertificate;
+        }
+
+        [CmifCommand(0)]
+        public Result Issue()
+        {
+            throw new NotImplementedException();
+        }
+
+        [CmifCommand(1)]
+        public Result SetApplicationProcessProperty(ulong pid, ApplicationProcessProperty applicationProcessProperty)
+        {
+            if (_forCertificate)
+            {
+                return ArpResult.DataAlreadyBound;
+            }
+
+            if (pid == 0)
+            {
+                return ArpResult.InvalidPid;
+            }
+
+            _applicationInstanceManager.Entries[_applicationInstanceId].Pid = pid;
+            _applicationInstanceManager.Entries[_applicationInstanceId].ProcessProperty = applicationProcessProperty;
+
+            return Result.Success;
+        }
+
+        [CmifCommand(2)]
+        public Result DeleteApplicationProcessProperty()
+        {
+            if (_forCertificate)
+            {
+                return ArpResult.DataAlreadyBound;
+            }
+
+            _applicationInstanceManager.Entries[_applicationInstanceId].ProcessProperty = new ApplicationProcessProperty();
+
+            return Result.Success;
+        }
+
+        [CmifCommand(3)]
+        public Result SetApplicationCertificate([Buffer(HipcBufferFlags.In | HipcBufferFlags.AutoSelect)] ApplicationCertificate applicationCertificate)
+        {
+            if (!_forCertificate)
+            {
+                return ArpResult.DataAlreadyBound;
+            }
+
+            _applicationInstanceManager.Entries[_applicationInstanceId].Certificate = applicationCertificate;
+
+            return Result.Success;
+        }
+    }
+}

+ 75 - 0
src/Ryujinx.Horizon/Arp/Ipc/Writer.cs

@@ -0,0 +1,75 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Arp;
+using Ryujinx.Horizon.Sdk.Arp.Detail;
+using Ryujinx.Horizon.Sdk.OsTypes;
+using Ryujinx.Horizon.Sdk.Sf;
+
+namespace Ryujinx.Horizon.Arp.Ipc
+{
+    partial class Writer : IWriter, IServiceObject
+    {
+        private readonly ApplicationInstanceManager _applicationInstanceManager;
+
+        public Writer(ApplicationInstanceManager applicationInstanceManager)
+        {
+            _applicationInstanceManager = applicationInstanceManager;
+        }
+
+        [CmifCommand(0)]
+        public Result AcquireRegistrar(out IRegistrar registrar)
+        {
+            if (_applicationInstanceManager.Entries[0] != null)
+            {
+                if (_applicationInstanceManager.Entries[1] != null)
+                {
+                    registrar = null;
+
+                    return ArpResult.NoFreeInstance;
+                }
+                else
+                {
+                    _applicationInstanceManager.Entries[1] = new ApplicationInstance();
+
+                    registrar = new Registrar(_applicationInstanceManager.Entries[1]);
+                }
+            }
+            else
+            {
+                _applicationInstanceManager.Entries[0] = new ApplicationInstance();
+
+                registrar = new Registrar(_applicationInstanceManager.Entries[0]);
+            }
+
+            return Result.Success;
+        }
+
+        [CmifCommand(1)]
+        public Result UnregisterApplicationInstance(ulong applicationInstanceId)
+        {
+            if (_applicationInstanceManager.Entries[applicationInstanceId] != null)
+            {
+                _applicationInstanceManager.Entries[applicationInstanceId] = null;
+            }
+
+            Os.SignalSystemEvent(ref _applicationInstanceManager.SystemEvent);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(2)]
+        public Result AcquireApplicationProcessPropertyUpdater(out IUpdater updater, ulong applicationInstanceId)
+        {
+            updater = new Updater(_applicationInstanceManager, applicationInstanceId, false);
+
+            return Result.Success;
+        }
+
+        [CmifCommand(3)]
+        public Result AcquireApplicationCertificateUpdater(out IUpdater updater, ulong applicationInstanceId)
+        {
+            updater = new Updater(_applicationInstanceManager, applicationInstanceId, true);
+
+            return Result.Success;
+        }
+    }
+}

+ 17 - 4
src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs

@@ -5,6 +5,7 @@ using Ryujinx.Common.Utilities;
 using Ryujinx.Horizon.Common;
 using Ryujinx.Horizon.Prepo.Types;
 using Ryujinx.Horizon.Sdk.Account;
+using Ryujinx.Horizon.Sdk.Arp;
 using Ryujinx.Horizon.Sdk.Prepo;
 using Ryujinx.Horizon.Sdk.Sf;
 using Ryujinx.Horizon.Sdk.Sf.Hipc;
@@ -22,14 +23,16 @@ namespace Ryujinx.Horizon.Prepo.Ipc
             System,
         }
 
+        private readonly ArpApi _arp;
         private readonly PrepoServicePermissionLevel _permissionLevel;
         private ulong _systemSessionId;
 
         private bool _immediateTransmissionEnabled;
         private bool _userAgreementCheckEnabled = true;
 
-        public PrepoService(PrepoServicePermissionLevel permissionLevel)
+        public PrepoService(ArpApi arp, PrepoServicePermissionLevel permissionLevel)
         {
+            _arp = arp;
             _permissionLevel = permissionLevel;
         }
 
@@ -165,7 +168,7 @@ namespace Ryujinx.Horizon.Prepo.Ipc
             return PrepoResult.PermissionDenied;
         }
 
-        private static Result ProcessPlayReport(PlayReportKind playReportKind, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid, Uid userId, bool withUserId = false, ApplicationId applicationId = default)
+        private Result ProcessPlayReport(PlayReportKind playReportKind, ReadOnlySpan<byte> gameRoomBuffer, ReadOnlySpan<byte> reportBuffer, ulong pid, Uid userId, bool withUserId = false, ApplicationId applicationId = default)
         {
             if (withUserId)
             {
@@ -199,8 +202,8 @@ namespace Ryujinx.Horizon.Prepo.Ipc
             builder.AppendLine("PlayReport log:");
             builder.AppendLine($" Kind: {playReportKind}");
 
-            // NOTE: The service calls arp:r using the pid to get the application id, if it fails PrepoResult.InvalidPid is returned.
-            //       Reports are stored internally and an event is signaled to transmit them.
+            // NOTE: Reports are stored internally and an event is signaled to transmit them.
+
             if (pid != 0)
             {
                 builder.AppendLine($" Pid: {pid}");
@@ -210,6 +213,16 @@ namespace Ryujinx.Horizon.Prepo.Ipc
                 builder.AppendLine($" ApplicationId: {applicationId}");
             }
 
+            Result result = _arp.GetApplicationInstanceId(out ulong applicationInstanceId, pid);
+            if (result.IsFailure)
+            {
+                return PrepoResult.InvalidPid;
+            }
+
+            _arp.GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, applicationInstanceId).AbortOnFailure();
+
+            builder.AppendLine($" ApplicationVersion: {applicationLaunchProperty.Version}");
+
             if (!userId.IsNull)
             {
                 builder.AppendLine($" UserId: {userId}");

+ 6 - 1
src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs

@@ -1,4 +1,5 @@
 using Ryujinx.Horizon.Prepo.Types;
+using Ryujinx.Horizon.Sdk.Arp;
 using Ryujinx.Horizon.Sdk.Sf.Hipc;
 using Ryujinx.Horizon.Sdk.Sm;
 
@@ -17,16 +18,19 @@ namespace Ryujinx.Horizon.Prepo
         private static readonly ManagerOptions _managerOptions = new(PointerBufferSize, MaxDomains, MaxDomainObjects, false);
 
         private SmApi _sm;
+        private ArpApi _arp;
         private PrepoServerManager _serverManager;
 
         public void Initialize()
         {
             HeapAllocator allocator = new();
 
+            _arp = new ArpApi(allocator);
+
             _sm = new SmApi();
             _sm.Initialize().AbortOnFailure();
 
-            _serverManager = new PrepoServerManager(allocator, _sm, MaxPortsCount, _managerOptions, TotalMaxSessionsCount);
+            _serverManager = new PrepoServerManager(allocator, _sm, _arp, MaxPortsCount, _managerOptions, TotalMaxSessionsCount);
 
 #pragma warning disable IDE0055 // Disable formatting
             _serverManager.RegisterServer((int)PrepoPortIndex.Admin,   ServiceName.Encode("prepo:a"),  MaxSessionsCount); // 1.0.0-5.1.0
@@ -45,6 +49,7 @@ namespace Ryujinx.Horizon.Prepo
 
         public void Shutdown()
         {
+            _arp.Dispose();
             _serverManager.Dispose();
         }
     }

+ 11 - 7
src/Ryujinx.Horizon/Prepo/PrepoServerManager.cs

@@ -1,6 +1,7 @@
 using Ryujinx.Horizon.Common;
 using Ryujinx.Horizon.Prepo.Ipc;
 using Ryujinx.Horizon.Prepo.Types;
+using Ryujinx.Horizon.Sdk.Arp;
 using Ryujinx.Horizon.Sdk.Sf.Hipc;
 using Ryujinx.Horizon.Sdk.Sm;
 using System;
@@ -9,8 +10,11 @@ namespace Ryujinx.Horizon.Prepo
 {
     class PrepoServerManager : ServerManager
     {
-        public PrepoServerManager(HeapAllocator allocator, SmApi sm, int maxPorts, ManagerOptions options, int maxSessions) : base(allocator, sm, maxPorts, options, maxSessions)
+        private readonly ArpApi _arp;
+
+        public PrepoServerManager(HeapAllocator allocator, SmApi sm, ArpApi arp, int maxPorts, ManagerOptions options, int maxSessions) : base(allocator, sm, maxPorts, options, maxSessions)
         {
+            _arp = arp;
         }
 
         protected override Result OnNeedsToAccept(int portIndex, Server server)
@@ -18,12 +22,12 @@ namespace Ryujinx.Horizon.Prepo
             return (PrepoPortIndex)portIndex switch
             {
 #pragma warning disable IDE0055 // Disable formatting
-                PrepoPortIndex.Admin   => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Admin)),
-                PrepoPortIndex.Admin2  => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Admin)),
-                PrepoPortIndex.Manager => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Manager)),
-                PrepoPortIndex.User    => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.User)),
-                PrepoPortIndex.System  => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.System)),
-                PrepoPortIndex.Debug   => AcceptImpl(server, new PrepoService(PrepoServicePermissionLevel.Debug)),
+                PrepoPortIndex.Admin   => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.Admin)),
+                PrepoPortIndex.Admin2  => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.Admin)),
+                PrepoPortIndex.Manager => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.Manager)),
+                PrepoPortIndex.User    => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.User)),
+                PrepoPortIndex.System  => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.System)),
+                PrepoPortIndex.Debug   => AcceptImpl(server, new PrepoService(_arp, PrepoServicePermissionLevel.Debug)),
                 _                      => throw new ArgumentOutOfRangeException(nameof(portIndex)),
 #pragma warning restore IDE0055
             };

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Arp/ApplicationCertificate.cs

@@ -0,0 +1,9 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    [StructLayout(LayoutKind.Sequential, Size = 0x528)]
+    public struct ApplicationCertificate
+    {
+    }
+}

+ 8 - 0
src/Ryujinx.Horizon/Sdk/Arp/ApplicationKind.cs

@@ -0,0 +1,8 @@
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    public enum ApplicationKind : byte
+    {
+        Application,
+        MicroApplication,
+    }
+}

+ 14 - 0
src/Ryujinx.Horizon/Sdk/Arp/ApplicationLaunchProperty.cs

@@ -0,0 +1,14 @@
+using Ryujinx.Horizon.Sdk.Ncm;
+
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    public struct ApplicationLaunchProperty
+    {
+        public ApplicationId ApplicationId;
+        public uint Version;
+        public StorageId Storage;
+        public StorageId PatchStorage;
+        public ApplicationKind ApplicationKind;
+        public byte Padding;
+    }
+}

+ 10 - 0
src/Ryujinx.Horizon/Sdk/Arp/ApplicationProcessProperty.cs

@@ -0,0 +1,10 @@
+using Ryujinx.Common.Memory;
+
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    public struct ApplicationProcessProperty
+    {
+        public byte ProgramIndex;
+        public Array15<byte> Unknown;
+    }
+}

+ 130 - 0
src/Ryujinx.Horizon/Sdk/Arp/ArpApi.cs

@@ -0,0 +1,130 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Ns;
+using Ryujinx.Horizon.Sdk.Sf.Cmif;
+using Ryujinx.Horizon.Sdk.Sf.Hipc;
+using Ryujinx.Horizon.Sdk.Sm;
+using System;
+using System.Runtime.CompilerServices;
+
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    class ArpApi : IDisposable
+    {
+        private const string ArpRName = "arp:r";
+
+        private readonly HeapAllocator _allocator;
+        private int _sessionHandle;
+
+        public ArpApi(HeapAllocator allocator)
+        {
+            _allocator = allocator;
+        }
+
+        private void InitializeArpRService()
+        {
+            if (_sessionHandle == 0)
+            {
+                using var smApi = new SmApi();
+
+                smApi.Initialize();
+                smApi.GetServiceHandle(out _sessionHandle, ServiceName.Encode(ArpRName)).AbortOnFailure();
+            }
+        }
+
+        public Result GetApplicationInstanceId(out ulong applicationInstanceId, ulong applicationPid)
+        {
+            Span<byte> data = stackalloc byte[8];
+            SpanWriter writer = new(data);
+
+            writer.Write(applicationPid);
+
+            InitializeArpRService();
+
+            Result result = ServiceUtil.SendRequest(out CmifResponse response, _sessionHandle, 3, sendPid: false, data);
+            if (result.IsFailure)
+            {
+                applicationInstanceId = 0;
+
+                return result;
+            }
+
+            SpanReader reader = new(response.Data);
+
+            applicationInstanceId = reader.Read<ulong>();
+
+            return Result.Success;
+        }
+
+        public Result GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, ulong applicationInstanceId)
+        {
+            applicationLaunchProperty = default;
+
+            Span<byte> data = stackalloc byte[8];
+            SpanWriter writer = new(data);
+
+            writer.Write(applicationInstanceId);
+
+            InitializeArpRService();
+
+            Result result = ServiceUtil.SendRequest(out CmifResponse response, _sessionHandle, 0, sendPid: false, data);
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            SpanReader reader = new(response.Data);
+
+            applicationLaunchProperty = reader.Read<ApplicationLaunchProperty>();
+
+            return Result.Success;
+        }
+
+        public Result GetApplicationControlProperty(out ApplicationControlProperty applicationControlProperty, ulong applicationInstanceId)
+        {
+            applicationControlProperty = default;
+
+            Span<byte> data = stackalloc byte[8];
+            SpanWriter writer = new(data);
+
+            writer.Write(applicationInstanceId);
+
+            ulong bufferSize = (ulong)Unsafe.SizeOf<ApplicationControlProperty>();
+            ulong bufferAddress = _allocator.Allocate(bufferSize);
+
+            InitializeArpRService();
+
+            Result result = ServiceUtil.SendRequest(
+                out CmifResponse response,
+                _sessionHandle,
+                1,
+                sendPid: false,
+                data,
+                stackalloc[] { HipcBufferFlags.Out | HipcBufferFlags.MapAlias | HipcBufferFlags.FixedSize },
+                stackalloc[] { new PointerAndSize(bufferAddress, bufferSize) });
+
+            if (result.IsFailure)
+            {
+                return result;
+            }
+
+            applicationControlProperty = HorizonStatic.AddressSpace.Read<ApplicationControlProperty>(bufferAddress);
+
+            _allocator.Free(bufferAddress, bufferSize);
+
+            return Result.Success;
+        }
+
+        public void Dispose()
+        {
+            if (_sessionHandle != 0)
+            {
+                HorizonStatic.Syscall.CloseHandle(_sessionHandle);
+
+                _sessionHandle = 0;
+            }
+
+            GC.SuppressFinalize(this);
+        }
+    }
+}

+ 17 - 0
src/Ryujinx.Horizon/Sdk/Arp/ArpResult.cs

@@ -0,0 +1,17 @@
+using Ryujinx.Horizon.Common;
+
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    static class ArpResult
+    {
+        private const int ModuleId = 157;
+
+        public static Result InvalidArgument => new(ModuleId, 30);
+        public static Result InvalidPid => new(ModuleId, 31);
+        public static Result InvalidPointer => new(ModuleId, 32);
+        public static Result DataAlreadyBound => new(ModuleId, 42);
+        public static Result AllocationFailed => new(ModuleId, 63);
+        public static Result NoFreeInstance => new(ModuleId, 101);
+        public static Result InvalidInstanceId => new(ModuleId, 102);
+    }
+}

+ 13 - 0
src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstance.cs

@@ -0,0 +1,13 @@
+using Ryujinx.Horizon.Sdk.Ns;
+
+namespace Ryujinx.Horizon.Sdk.Arp.Detail
+{
+    class ApplicationInstance
+    {
+        public ulong Pid { get; set; }
+        public ApplicationLaunchProperty? LaunchProperty { get; set; }
+        public ApplicationProcessProperty? ProcessProperty { get; set; }
+        public ApplicationControlProperty? ControlProperty { get; set; }
+        public ApplicationCertificate? Certificate { get; set; }
+    }
+}

+ 31 - 0
src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstanceManager.cs

@@ -0,0 +1,31 @@
+using Ryujinx.Horizon.Sdk.OsTypes;
+using System;
+using System.Threading;
+
+namespace Ryujinx.Horizon.Sdk.Arp.Detail
+{
+    class ApplicationInstanceManager : IDisposable
+    {
+        private int _disposalState;
+
+        public SystemEventType SystemEvent;
+        public int EventHandle;
+
+        public readonly ApplicationInstance[] Entries = new ApplicationInstance[2];
+
+        public ApplicationInstanceManager()
+        {
+            Os.CreateSystemEvent(out SystemEvent, EventClearMode.ManualClear, true).AbortOnFailure();
+
+            EventHandle = Os.GetReadableHandleOfSystemEvent(ref SystemEvent);
+        }
+
+        public void Dispose()
+        {
+            if (EventHandle != 0 && Interlocked.Exchange(ref _disposalState, 1) == 0)
+            {
+                Os.DestroySystemEvent(ref SystemEvent);
+            }
+        }
+    }
+}

+ 18 - 0
src/Ryujinx.Horizon/Sdk/Arp/IReader.cs

@@ -0,0 +1,18 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Ns;
+using System;
+
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    public interface IReader
+    {
+        public Result GetApplicationLaunchProperty(out ApplicationLaunchProperty applicationLaunchProperty, ulong applicationInstanceId);
+        public Result GetApplicationControlProperty(out ApplicationControlProperty applicationControlProperty, ulong applicationInstanceId);
+        public Result GetApplicationProcessProperty(out ApplicationProcessProperty applicationControlProperty, ulong applicationInstanceId);
+        public Result GetApplicationInstanceId(out ulong applicationInstanceId, ulong pid);
+        public Result GetApplicationInstanceUnregistrationNotifier(out IUnregistrationNotifier unregistrationNotifier);
+        public Result ListApplicationInstanceId(out int count, Span<ulong> applicationInstanceIdList);
+        public Result GetMicroApplicationInstanceId(out ulong MicroApplicationInstanceId, ulong pid);
+        public Result GetApplicationCertificate(out ApplicationCertificate applicationCertificate, ulong applicationInstanceId);
+    }
+}

+ 12 - 0
src/Ryujinx.Horizon/Sdk/Arp/IRegistrar.cs

@@ -0,0 +1,12 @@
+using Ryujinx.Horizon.Common;
+using Ryujinx.Horizon.Sdk.Ns;
+
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    public interface IRegistrar
+    {
+        public Result Issue(out ulong applicationInstanceId);
+        public Result SetApplicationLaunchProperty(ApplicationLaunchProperty applicationLaunchProperty);
+        public Result SetApplicationControlProperty(in ApplicationControlProperty applicationControlProperty);
+    }
+}

+ 9 - 0
src/Ryujinx.Horizon/Sdk/Arp/IUnregistrationNotifier.cs

@@ -0,0 +1,9 @@
+using Ryujinx.Horizon.Common;
+
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    public interface IUnregistrationNotifier
+    {
+        public Result GetReadableHandle(out int readableHandle);
+    }
+}

+ 12 - 0
src/Ryujinx.Horizon/Sdk/Arp/IUpdater.cs

@@ -0,0 +1,12 @@
+using Ryujinx.Horizon.Common;
+
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    public interface IUpdater
+    {
+        public Result Issue();
+        public Result SetApplicationProcessProperty(ulong pid, ApplicationProcessProperty applicationProcessProperty);
+        public Result DeleteApplicationProcessProperty();
+        public Result SetApplicationCertificate(ApplicationCertificate applicationCertificate);
+    }
+}

+ 12 - 0
src/Ryujinx.Horizon/Sdk/Arp/IWriter.cs

@@ -0,0 +1,12 @@
+using Ryujinx.Horizon.Common;
+
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    public interface IWriter
+    {
+        public Result AcquireRegistrar(out IRegistrar registrar);
+        public Result UnregisterApplicationInstance(ulong applicationInstanceId);
+        public Result AcquireApplicationProcessPropertyUpdater(out IUpdater updater, ulong applicationInstanceId);
+        public Result AcquireApplicationCertificateUpdater(out IUpdater updater, ulong applicationInstanceId);
+    }
+}

+ 1 - 1
src/Ryujinx.Horizon/Sdk/Ncm/ApplicationId.cs

@@ -1,6 +1,6 @@
 namespace Ryujinx.Horizon.Sdk.Ncm
 {
-    readonly struct ApplicationId
+    public readonly struct ApplicationId
     {
         public readonly ulong Id;
 

+ 13 - 0
src/Ryujinx.Horizon/Sdk/Ncm/StorageId.cs

@@ -0,0 +1,13 @@
+namespace Ryujinx.Horizon.Sdk.Ncm
+{
+    public enum StorageId : byte
+    {
+        None,
+        Host,
+        GameCard,
+        BuiltInSystem,
+        BuiltInUser,
+        SdCard,
+        Any,
+    }
+}

+ 309 - 0
src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs

@@ -0,0 +1,309 @@
+using Ryujinx.Common.Memory;
+using Ryujinx.Horizon.Sdk.Arp.Detail;
+using System;
+using System.Runtime.CompilerServices;
+using System.Runtime.InteropServices;
+using System.Text;
+
+namespace Ryujinx.Horizon.Sdk.Ns
+{
+    public struct ApplicationControlProperty
+    {
+        public Array16<ApplicationTitle> Title;
+        public Array37<byte> Isbn;
+        public StartupUserAccountValue StartupUserAccount;
+        public UserAccountSwitchLockValue UserAccountSwitchLock;
+        public AddOnContentRegistrationTypeValue AddOnContentRegistrationType;
+        public AttributeFlagValue AttributeFlag;
+        public uint SupportedLanguageFlag;
+        public ParentalControlFlagValue ParentalControlFlag;
+        public ScreenshotValue Screenshot;
+        public VideoCaptureValue VideoCapture;
+        public DataLossConfirmationValue DataLossConfirmation;
+        public PlayLogPolicyValue PlayLogPolicy;
+        public ulong PresenceGroupId;
+        public Array32<sbyte> RatingAge;
+        public Array16<byte> DisplayVersion;
+        public ulong AddOnContentBaseId;
+        public ulong SaveDataOwnerId;
+        public long UserAccountSaveDataSize;
+        public long UserAccountSaveDataJournalSize;
+        public long DeviceSaveDataSize;
+        public long DeviceSaveDataJournalSize;
+        public long BcatDeliveryCacheStorageSize;
+        public Array8<byte> ApplicationErrorCodeCategory;
+        public Array8<ulong> LocalCommunicationId;
+        public LogoTypeValue LogoType;
+        public LogoHandlingValue LogoHandling;
+        public RuntimeAddOnContentInstallValue RuntimeAddOnContentInstall;
+        public RuntimeParameterDeliveryValue RuntimeParameterDelivery;
+        public Array2<byte> Reserved30F4;
+        public CrashReportValue CrashReport;
+        public HdcpValue Hdcp;
+        public ulong SeedForPseudoDeviceId;
+        public Array65<byte> BcatPassphrase;
+        public StartupUserAccountOptionFlagValue StartupUserAccountOption;
+        public Array6<byte> ReservedForUserAccountSaveDataOperation;
+        public long UserAccountSaveDataSizeMax;
+        public long UserAccountSaveDataJournalSizeMax;
+        public long DeviceSaveDataSizeMax;
+        public long DeviceSaveDataJournalSizeMax;
+        public long TemporaryStorageSize;
+        public long CacheStorageSize;
+        public long CacheStorageJournalSize;
+        public long CacheStorageDataAndJournalSizeMax;
+        public ushort CacheStorageIndexMax;
+        public byte Reserved318A;
+        public byte RuntimeUpgrade;
+        public uint SupportingLimitedLicenses;
+        public Array16<ulong> PlayLogQueryableApplicationId;
+        public PlayLogQueryCapabilityValue PlayLogQueryCapability;
+        public RepairFlagValue RepairFlag;
+        public byte ProgramIndex;
+        public RequiredNetworkServiceLicenseOnLaunchValue RequiredNetworkServiceLicenseOnLaunchFlag;
+        public Array4<byte> Reserved3214;
+        public ApplicationNeighborDetectionClientConfiguration NeighborDetectionClientConfiguration;
+        public ApplicationJitConfiguration JitConfiguration;
+        public RequiredAddOnContentsSetBinaryDescriptor RequiredAddOnContentsSetBinaryDescriptors;
+        public PlayReportPermissionValue PlayReportPermission;
+        public CrashScreenshotForProdValue CrashScreenshotForProd;
+        public CrashScreenshotForDevValue CrashScreenshotForDev;
+        public byte ContentsAvailabilityTransitionPolicy;
+        public Array4<byte> Reserved3404;
+        public AccessibleLaunchRequiredVersionValue AccessibleLaunchRequiredVersion;
+        public ByteArray3000 Reserved3448;
+
+        public readonly string IsbnString => Encoding.UTF8.GetString(Isbn.AsSpan()).TrimEnd('\0');
+        public readonly string DisplayVersionString => Encoding.UTF8.GetString(DisplayVersion.AsSpan()).TrimEnd('\0');
+        public readonly string ApplicationErrorCodeCategoryString => Encoding.UTF8.GetString(ApplicationErrorCodeCategory.AsSpan()).TrimEnd('\0');
+        public readonly string BcatPassphraseString => Encoding.UTF8.GetString(BcatPassphrase.AsSpan()).TrimEnd('\0');
+
+        public struct ApplicationTitle
+        {
+            public ByteArray512 Name;
+            public Array256<byte> Publisher;
+
+            public readonly string NameString => Encoding.UTF8.GetString(Name.AsSpan()).TrimEnd('\0');
+            public readonly string PublisherString => Encoding.UTF8.GetString(Publisher.AsSpan()).TrimEnd('\0');
+        }
+
+        public struct ApplicationNeighborDetectionClientConfiguration
+        {
+            public ApplicationNeighborDetectionGroupConfiguration SendGroupConfiguration;
+            public Array16<ApplicationNeighborDetectionGroupConfiguration> ReceivableGroupConfigurations;
+        }
+
+        public struct ApplicationNeighborDetectionGroupConfiguration
+        {
+            public ulong GroupId;
+            public Array16<byte> Key;
+        }
+
+        public struct ApplicationJitConfiguration
+        {
+            public JitConfigurationFlag Flags;
+            public long MemorySize;
+        }
+
+        public struct RequiredAddOnContentsSetBinaryDescriptor
+        {
+            public Array32<ushort> Descriptors;
+        }
+
+        public struct AccessibleLaunchRequiredVersionValue
+        {
+            public Array8<ulong> ApplicationId;
+        }
+
+        public enum Language
+        {
+            AmericanEnglish = 0,
+            BritishEnglish = 1,
+            Japanese = 2,
+            French = 3,
+            German = 4,
+            LatinAmericanSpanish = 5,
+            Spanish = 6,
+            Italian = 7,
+            Dutch = 8,
+            CanadianFrench = 9,
+            Portuguese = 10,
+            Russian = 11,
+            Korean = 12,
+            TraditionalChinese = 13,
+            SimplifiedChinese = 14,
+            BrazilianPortuguese = 15,
+        }
+
+        public enum Organization
+        {
+            CERO = 0,
+            GRACGCRB = 1,
+            GSRMR = 2,
+            ESRB = 3,
+            ClassInd = 4,
+            USK = 5,
+            PEGI = 6,
+            PEGIPortugal = 7,
+            PEGIBBFC = 8,
+            Russian = 9,
+            ACB = 10,
+            OFLC = 11,
+            IARCGeneric = 12,
+        }
+
+        public enum StartupUserAccountValue : byte
+        {
+            None = 0,
+            Required = 1,
+            RequiredWithNetworkServiceAccountAvailable = 2,
+        }
+
+        public enum UserAccountSwitchLockValue : byte
+        {
+            Disable = 0,
+            Enable = 1,
+        }
+
+        public enum AddOnContentRegistrationTypeValue : byte
+        {
+            AllOnLaunch = 0,
+            OnDemand = 1,
+        }
+
+        [Flags]
+        public enum AttributeFlagValue
+        {
+            None = 0,
+            Demo = 1 << 0,
+            RetailInteractiveDisplay = 1 << 1,
+        }
+
+        public enum ParentalControlFlagValue
+        {
+            None = 0,
+            FreeCommunication = 1,
+        }
+
+        public enum ScreenshotValue : byte
+        {
+            Allow = 0,
+            Deny = 1,
+        }
+
+        public enum VideoCaptureValue : byte
+        {
+            Disable = 0,
+            Manual = 1,
+            Enable = 2,
+        }
+
+        public enum DataLossConfirmationValue : byte
+        {
+            None = 0,
+            Required = 1,
+        }
+
+        public enum PlayLogPolicyValue : byte
+        {
+            Open = 0,
+            LogOnly = 1,
+            None = 2,
+            Closed = 3,
+            All = Open,
+        }
+
+        public enum LogoTypeValue : byte
+        {
+            LicensedByNintendo = 0,
+            DistributedByNintendo = 1,
+            Nintendo = 2,
+        }
+
+        public enum LogoHandlingValue : byte
+        {
+            Auto = 0,
+            Manual = 1,
+        }
+
+        public enum RuntimeAddOnContentInstallValue : byte
+        {
+            Deny = 0,
+            AllowAppend = 1,
+            AllowAppendButDontDownloadWhenUsingNetwork = 2,
+        }
+
+        public enum RuntimeParameterDeliveryValue : byte
+        {
+            Always = 0,
+            AlwaysIfUserStateMatched = 1,
+            OnRestart = 2,
+        }
+
+        public enum CrashReportValue : byte
+        {
+            Deny = 0,
+            Allow = 1,
+        }
+
+        public enum HdcpValue : byte
+        {
+            None = 0,
+            Required = 1,
+        }
+
+        [Flags]
+        public enum StartupUserAccountOptionFlagValue : byte
+        {
+            None = 0,
+            IsOptional = 1 << 0,
+        }
+
+        public enum PlayLogQueryCapabilityValue : byte
+        {
+            None = 0,
+            WhiteList = 1,
+            All = 2,
+        }
+
+        [Flags]
+        public enum RepairFlagValue : byte
+        {
+            None = 0,
+            SuppressGameCardAccess = 1 << 0,
+        }
+
+        [Flags]
+        public enum RequiredNetworkServiceLicenseOnLaunchValue : byte
+        {
+            None = 0,
+            Common = 1 << 0,
+        }
+
+        [Flags]
+        public enum JitConfigurationFlag : ulong
+        {
+            None = 0,
+            Enabled = 1 << 0,
+        }
+
+        [Flags]
+        public enum PlayReportPermissionValue : byte
+        {
+            None = 0,
+            TargetMarketing = 1 << 0,
+        }
+
+        public enum CrashScreenshotForProdValue : byte
+        {
+            Deny = 0,
+            Allow = 1,
+        }
+
+        public enum CrashScreenshotForDevValue : byte
+        {
+            Deny = 0,
+            Allow = 1,
+        }
+    }
+}

+ 249 - 0
src/Ryujinx.Horizon/Sdk/ServiceUtil.cs

@@ -35,5 +35,254 @@ namespace Ryujinx.Horizon.Sdk
 
             return CmifMessage.ParseResponse(out response, HorizonStatic.AddressSpace.GetWritableRegion(tlsAddress, tlsSize).Memory.Span, false, 0);
         }
+
+        public static Result SendRequest(
+            out CmifResponse response,
+            int sessionHandle,
+            uint requestId,
+            bool sendPid,
+            scoped ReadOnlySpan<byte> data,
+            ReadOnlySpan<HipcBufferFlags> bufferFlags,
+            ReadOnlySpan<PointerAndSize> buffers)
+        {
+            ulong tlsAddress = HorizonStatic.ThreadContext.TlsAddress;
+            int tlsSize = Api.TlsMessageBufferSize;
+
+            using (var tlsRegion = HorizonStatic.AddressSpace.GetWritableRegion(tlsAddress, tlsSize))
+            {
+                CmifRequestFormat format = new()
+                {
+                    DataSize = data.Length,
+                    RequestId = requestId,
+                    SendPid = sendPid,
+                };
+
+                for (int index = 0; index < bufferFlags.Length; index++)
+                {
+                    FormatProcessBuffer(ref format, bufferFlags[index]);
+                }
+
+                CmifRequest request = CmifMessage.CreateRequest(tlsRegion.Memory.Span, format);
+
+                for (int index = 0; index < buffers.Length; index++)
+                {
+                    RequestProcessBuffer(ref request, buffers[index], bufferFlags[index]);
+                }
+
+                data.CopyTo(request.Data);
+            }
+
+            Result result = HorizonStatic.Syscall.SendSyncRequest(sessionHandle);
+
+            if (result.IsFailure)
+            {
+                response = default;
+
+                return result;
+            }
+
+            return CmifMessage.ParseResponse(out response, HorizonStatic.AddressSpace.GetWritableRegion(tlsAddress, tlsSize).Memory.Span, false, 0);
+        }
+
+        private static void FormatProcessBuffer(ref CmifRequestFormat format, HipcBufferFlags flags)
+        {
+            if (flags == 0)
+            {
+                return;
+            }
+
+            bool isIn = flags.HasFlag(HipcBufferFlags.In);
+            bool isOut = flags.HasFlag(HipcBufferFlags.Out);
+
+            if (flags.HasFlag(HipcBufferFlags.AutoSelect))
+            {
+                if (isIn)
+                {
+                    format.InAutoBuffersCount++;
+                }
+
+                if (isOut)
+                {
+                    format.OutAutoBuffersCount++;
+                }
+            }
+            else if (flags.HasFlag(HipcBufferFlags.Pointer))
+            {
+                if (isIn)
+                {
+                    format.InPointersCount++;
+                }
+
+                if (isOut)
+                {
+                    if (flags.HasFlag(HipcBufferFlags.FixedSize))
+                    {
+                        format.OutFixedPointersCount++;
+                    }
+                    else
+                    {
+                        format.OutPointersCount++;
+                    }
+                }
+            }
+            else if (flags.HasFlag(HipcBufferFlags.MapAlias))
+            {
+                if (isIn && isOut)
+                {
+                    format.InOutBuffersCount++;
+                }
+                else if (isIn)
+                {
+                    format.InBuffersCount++;
+                }
+                else
+                {
+                    format.OutBuffersCount++;
+                }
+            }
+        }
+
+        private static void RequestProcessBuffer(ref CmifRequest request, PointerAndSize buffer, HipcBufferFlags flags)
+        {
+            if (flags == 0)
+            {
+                return;
+            }
+
+            bool isIn = flags.HasFlag(HipcBufferFlags.In);
+            bool isOut = flags.HasFlag(HipcBufferFlags.Out);
+
+            if (flags.HasFlag(HipcBufferFlags.AutoSelect))
+            {
+                HipcBufferMode mode = HipcBufferMode.Normal;
+
+                if (flags.HasFlag(HipcBufferFlags.MapTransferAllowsNonSecure))
+                {
+                    mode = HipcBufferMode.NonSecure;
+                }
+
+                if (flags.HasFlag(HipcBufferFlags.MapTransferAllowsNonDevice))
+                {
+                    mode = HipcBufferMode.NonDevice;
+                }
+
+                if (isIn)
+                {
+                    RequestInAutoBuffer(ref request, buffer.Address, buffer.Size, mode);
+                }
+
+                if (isOut)
+                {
+                    RequestOutAutoBuffer(ref request, buffer.Address, buffer.Size, mode);
+                }
+            }
+            else if (flags.HasFlag(HipcBufferFlags.Pointer))
+            {
+                if (isIn)
+                {
+                    RequestInPointer(ref request, buffer.Address, buffer.Size);
+                }
+
+                if (isOut)
+                {
+                    if (flags.HasFlag(HipcBufferFlags.FixedSize))
+                    {
+                        RequestOutFixedPointer(ref request, buffer.Address, buffer.Size);
+                    }
+                    else
+                    {
+                        RequestOutPointer(ref request, buffer.Address, buffer.Size);
+                    }
+                }
+            }
+            else if (flags.HasFlag(HipcBufferFlags.MapAlias))
+            {
+                HipcBufferMode mode = HipcBufferMode.Normal;
+
+                if (flags.HasFlag(HipcBufferFlags.MapTransferAllowsNonSecure))
+                {
+                    mode = HipcBufferMode.NonSecure;
+                }
+
+                if (flags.HasFlag(HipcBufferFlags.MapTransferAllowsNonDevice))
+                {
+                    mode = HipcBufferMode.NonDevice;
+                }
+
+                if (isIn && isOut)
+                {
+                    RequestInOutBuffer(ref request, buffer.Address, buffer.Size, mode);
+                }
+                else if (isIn)
+                {
+                    RequestInBuffer(ref request, buffer.Address, buffer.Size, mode);
+                }
+                else
+                {
+                    RequestOutBuffer(ref request, buffer.Address, buffer.Size, mode);
+                }
+            }
+        }
+
+        private static void RequestInAutoBuffer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize, HipcBufferMode mode)
+        {
+            if (request.ServerPointerSize != 0 && bufferSize <= (ulong)request.ServerPointerSize)
+            {
+                RequestInPointer(ref request, bufferAddress, bufferSize);
+                RequestInBuffer(ref request, 0UL, 0UL, mode);
+            }
+            else
+            {
+                RequestInPointer(ref request, 0UL, 0UL);
+                RequestInBuffer(ref request, bufferAddress, bufferSize, mode);
+            }
+        }
+
+        private static void RequestOutAutoBuffer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize, HipcBufferMode mode)
+        {
+            if (request.ServerPointerSize != 0 && bufferSize <= (ulong)request.ServerPointerSize)
+            {
+                RequestOutPointer(ref request, bufferAddress, bufferSize);
+                RequestOutBuffer(ref request, 0UL, 0UL, mode);
+            }
+            else
+            {
+                RequestOutPointer(ref request, 0UL, 0UL);
+                RequestOutBuffer(ref request, bufferAddress, bufferSize, mode);
+            }
+        }
+
+        private static void RequestInBuffer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize, HipcBufferMode mode)
+        {
+            request.Hipc.SendBuffers[request.SendBufferIndex++] = new HipcBufferDescriptor(bufferAddress, bufferSize, mode);
+        }
+
+        private static void RequestOutBuffer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize, HipcBufferMode mode)
+        {
+            request.Hipc.ReceiveBuffers[request.RecvBufferIndex++] = new HipcBufferDescriptor(bufferAddress, bufferSize, mode);
+        }
+
+        private static void RequestInOutBuffer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize, HipcBufferMode mode)
+        {
+            request.Hipc.ExchangeBuffers[request.ExchBufferIndex++] = new HipcBufferDescriptor(bufferAddress, bufferSize, mode);
+        }
+
+        private static void RequestInPointer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize)
+        {
+            request.Hipc.SendStatics[request.SendStaticIndex++] = new HipcStaticDescriptor(bufferAddress, (ushort)bufferSize, request.CurrentInPointerId++);
+            request.ServerPointerSize -= (int)bufferSize;
+        }
+
+        private static void RequestOutFixedPointer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize)
+        {
+            request.Hipc.ReceiveList[request.RecvListIndex++] = new HipcReceiveListEntry(bufferAddress, (ushort)bufferSize);
+            request.ServerPointerSize -= (int)bufferSize;
+        }
+
+        private static void RequestOutPointer(ref CmifRequest request, ulong bufferAddress, ulong bufferSize)
+        {
+            RequestOutFixedPointer(ref request, bufferAddress, bufferSize);
+            request.OutPointerSizes[request.OutPointerSizeIndex++] = (ushort)bufferSize;
+        }
     }
 }

+ 7 - 0
src/Ryujinx.Horizon/Sdk/Sf/Cmif/CmifRequest.cs

@@ -10,5 +10,12 @@ namespace Ryujinx.Horizon.Sdk.Sf.Cmif
         public Span<ushort> OutPointerSizes;
         public Span<uint> Objects;
         public int ServerPointerSize;
+        public int CurrentInPointerId;
+        public int SendBufferIndex;
+        public int RecvBufferIndex;
+        public int ExchBufferIndex;
+        public int SendStaticIndex;
+        public int RecvListIndex;
+        public int OutPointerSizeIndex;
     }
 }

+ 7 - 0
src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcBufferDescriptor.cs

@@ -11,5 +11,12 @@ namespace Ryujinx.Horizon.Sdk.Sf.Hipc
         public ulong Address => _addressLow | (((ulong)_word2 << 4) & 0xf00000000UL) | (((ulong)_word2 << 34) & 0x7000000000UL);
         public ulong Size => _sizeLow | ((ulong)_word2 << 8) & 0xf00000000UL;
         public HipcBufferMode Mode => (HipcBufferMode)(_word2 & 3);
+
+        public HipcBufferDescriptor(ulong address, ulong size, HipcBufferMode mode)
+        {
+            _sizeLow = (uint)size;
+            _addressLow = (uint)address;
+            _word2 = (uint)mode | ((uint)(address >> 34) & 0x1c) | ((uint)(size >> 32) << 24) | ((uint)(address >> 4) & 0xf0000000);
+        }
     }
 }

+ 6 - 0
src/Ryujinx.Horizon/ServiceTable.cs

@@ -1,3 +1,4 @@
+using Ryujinx.Horizon.Arp;
 using Ryujinx.Horizon.Bcat;
 using Ryujinx.Horizon.Hshl;
 using Ryujinx.Horizon.Ins;
@@ -8,6 +9,7 @@ using Ryujinx.Horizon.Ngc;
 using Ryujinx.Horizon.Ovln;
 using Ryujinx.Horizon.Prepo;
 using Ryujinx.Horizon.Psc;
+using Ryujinx.Horizon.Sdk.Arp;
 using Ryujinx.Horizon.Srepo;
 using Ryujinx.Horizon.Usb;
 using Ryujinx.Horizon.Wlan;
@@ -23,6 +25,9 @@ namespace Ryujinx.Horizon
 
         private readonly ManualResetEvent _servicesReadyEvent = new(false);
 
+        public IReader ArpReader { get; internal set; }
+        public IWriter ArpWriter { get; internal set; }
+
         public IEnumerable<ServiceEntry> GetServices(HorizonOptions options)
         {
             List<ServiceEntry> entries = new();
@@ -32,6 +37,7 @@ namespace Ryujinx.Horizon
                 entries.Add(new ServiceEntry(T.Main, this, options));
             }
 
+            RegisterService<ArpMain>();
             RegisterService<BcatMain>();
             RegisterService<HshlMain>();
             RegisterService<InsMain>();