From cd37c75b82f97ad5d3bf6317ffcde62c06a6e920 Mon Sep 17 00:00:00 2001
From: Ac_K <Acoustik666@gmail.com>
Date: Thu, 25 Jan 2024 23:06:53 +0100
Subject: [PATCH] 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
---
 .../Memory/StructArrayHelpers.cs              |  11 +
 .../Memory/StructByteArrayHelpers.cs          |  12 +
 src/Ryujinx.HLE/HOS/Services/Arp/IReader.cs   |   8 -
 src/Ryujinx.HLE/HOS/Services/Arp/IWriter.cs   |   8 -
 .../IParentalControlService.cs                |   2 -
 .../Extensions/FileSystemExtensions.cs        |   3 +-
 .../Extensions/LocalFileSystemExtensions.cs   |   2 +-
 .../Processes/Extensions/NcaExtensions.cs     |   2 +-
 .../Loaders/Processes/ProcessLoader.cs        |   5 +-
 .../Loaders/Processes/ProcessLoaderHelper.cs  |  23 +-
 src/Ryujinx.Horizon/Arp/ArpIpcServer.cs       |  61 ++++
 src/Ryujinx.Horizon/Arp/ArpMain.cs            |  20 ++
 src/Ryujinx.Horizon/Arp/Ipc/Reader.cs         | 135 ++++++++
 src/Ryujinx.Horizon/Arp/Ipc/Registrar.cs      |  52 +++
 .../Arp/Ipc/UnregistrationNotifier.cs         |  25 ++
 src/Ryujinx.Horizon/Arp/Ipc/Updater.cs        |  74 +++++
 src/Ryujinx.Horizon/Arp/Ipc/Writer.cs         |  75 +++++
 src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs |  21 +-
 src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs   |   7 +-
 .../Prepo/PrepoServerManager.cs               |  18 +-
 .../Sdk/Arp/ApplicationCertificate.cs         |   9 +
 .../Sdk/Arp/ApplicationKind.cs                |   8 +
 .../Sdk/Arp/ApplicationLaunchProperty.cs      |  14 +
 .../Sdk/Arp/ApplicationProcessProperty.cs     |  10 +
 src/Ryujinx.Horizon/Sdk/Arp/ArpApi.cs         | 130 ++++++++
 src/Ryujinx.Horizon/Sdk/Arp/ArpResult.cs      |  17 +
 .../Sdk/Arp/Detail/ApplicationInstance.cs     |  13 +
 .../Arp/Detail/ApplicationInstanceManager.cs  |  31 ++
 src/Ryujinx.Horizon/Sdk/Arp/IReader.cs        |  18 +
 src/Ryujinx.Horizon/Sdk/Arp/IRegistrar.cs     |  12 +
 .../Sdk/Arp/IUnregistrationNotifier.cs        |   9 +
 src/Ryujinx.Horizon/Sdk/Arp/IUpdater.cs       |  12 +
 src/Ryujinx.Horizon/Sdk/Arp/IWriter.cs        |  12 +
 src/Ryujinx.Horizon/Sdk/Ncm/ApplicationId.cs  |   2 +-
 src/Ryujinx.Horizon/Sdk/Ncm/StorageId.cs      |  13 +
 .../Sdk/Ns/ApplicationControlProperty.cs      | 309 ++++++++++++++++++
 src/Ryujinx.Horizon/Sdk/ServiceUtil.cs        | 249 ++++++++++++++
 .../Sdk/Sf/Cmif/CmifRequest.cs                |   7 +
 .../Sdk/Sf/Hipc/HipcBufferDescriptor.cs       |   7 +
 src/Ryujinx.Horizon/ServiceTable.cs           |   6 +
 40 files changed, 1415 insertions(+), 37 deletions(-)
 delete mode 100644 src/Ryujinx.HLE/HOS/Services/Arp/IReader.cs
 delete mode 100644 src/Ryujinx.HLE/HOS/Services/Arp/IWriter.cs
 create mode 100644 src/Ryujinx.Horizon/Arp/ArpIpcServer.cs
 create mode 100644 src/Ryujinx.Horizon/Arp/ArpMain.cs
 create mode 100644 src/Ryujinx.Horizon/Arp/Ipc/Reader.cs
 create mode 100644 src/Ryujinx.Horizon/Arp/Ipc/Registrar.cs
 create mode 100644 src/Ryujinx.Horizon/Arp/Ipc/UnregistrationNotifier.cs
 create mode 100644 src/Ryujinx.Horizon/Arp/Ipc/Updater.cs
 create mode 100644 src/Ryujinx.Horizon/Arp/Ipc/Writer.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/ApplicationCertificate.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/ApplicationKind.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/ApplicationLaunchProperty.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/ApplicationProcessProperty.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/ArpApi.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/ArpResult.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstance.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstanceManager.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/IReader.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/IRegistrar.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/IUnregistrationNotifier.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/IUpdater.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Arp/IWriter.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Ncm/StorageId.cs
 create mode 100644 src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs

diff --git a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs b/src/Ryujinx.Common/Memory/StructArrayHelpers.cs
index 94ce8d2f56..762c73889b 100644
--- a/src/Ryujinx.Common/Memory/StructArrayHelpers.cs
+++ b/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;
diff --git a/src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs b/src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs
index 3b0666628f..79b7d681a4 100644
--- a/src/Ryujinx.Common/Memory/StructByteArrayHelpers.cs
+++ b/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>
     {
diff --git a/src/Ryujinx.HLE/HOS/Services/Arp/IReader.cs b/src/Ryujinx.HLE/HOS/Services/Arp/IReader.cs
deleted file mode 100644
index 6113104213..0000000000
--- a/src/Ryujinx.HLE/HOS/Services/Arp/IReader.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Arp
-{
-    [Service("arp:r")]
-    class IReader : IpcService
-    {
-        public IReader(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Arp/IWriter.cs b/src/Ryujinx.HLE/HOS/Services/Arp/IWriter.cs
deleted file mode 100644
index 22d081b9b8..0000000000
--- a/src/Ryujinx.HLE/HOS/Services/Arp/IWriter.cs
+++ /dev/null
@@ -1,8 +0,0 @@
-namespace Ryujinx.HLE.HOS.Services.Arp
-{
-    [Service("arp:w")]
-    class IWriter : IpcService
-    {
-        public IWriter(ServiceCtx context) { }
-    }
-}
diff --git a/src/Ryujinx.HLE/HOS/Services/Pctl/ParentalControlServiceFactory/IParentalControlService.cs b/src/Ryujinx.HLE/HOS/Services/Pctl/ParentalControlServiceFactory/IParentalControlService.cs
index cf8c1f78d9..9b026a1c32 100644
--- a/src/Ryujinx.HLE/HOS/Services/Pctl/ParentalControlServiceFactory/IParentalControlService.cs
+++ b/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
             }
         }
 
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs
index 28f6fcff41..3904d660ed 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/FileSystemExtensions.cs
+++ b/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);
 
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
index 798a9261ed..39139ecca6 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/LocalFileSystemExtensions.cs
+++ b/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))
diff --git a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs b/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
index e369f4b046..e70fcb6fc4 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/Extensions/NcaExtensions.cs
+++ b/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)
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
index efeb9f6139..e5056c89a6 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoader.cs
+++ b/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);
 
diff --git a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs b/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
index a6a1d87e0b..fe2aaac6d8 100644
--- a/src/Ryujinx.HLE/Loaders/Processes/ProcessLoaderHelper.cs
+++ b/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)
diff --git a/src/Ryujinx.Horizon/Arp/ArpIpcServer.cs b/src/Ryujinx.Horizon/Arp/ArpIpcServer.cs
new file mode 100644
index 0000000000..6080b4750a
--- /dev/null
+++ b/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();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Arp/ArpMain.cs b/src/Ryujinx.Horizon/Arp/ArpMain.cs
new file mode 100644
index 0000000000..a28baa71a7
--- /dev/null
+++ b/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();
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Arp/Ipc/Reader.cs b/src/Ryujinx.Horizon/Arp/Ipc/Reader.cs
new file mode 100644
index 0000000000..de99c2ade8
--- /dev/null
+++ b/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;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Arp/Ipc/Registrar.cs b/src/Ryujinx.Horizon/Arp/Ipc/Registrar.cs
new file mode 100644
index 0000000000..841ab7601d
--- /dev/null
+++ b/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;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Arp/Ipc/UnregistrationNotifier.cs b/src/Ryujinx.Horizon/Arp/Ipc/UnregistrationNotifier.cs
new file mode 100644
index 0000000000..49f2b1ccea
--- /dev/null
+++ b/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;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Arp/Ipc/Updater.cs b/src/Ryujinx.Horizon/Arp/Ipc/Updater.cs
new file mode 100644
index 0000000000..f7531d712d
--- /dev/null
+++ b/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;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Arp/Ipc/Writer.cs b/src/Ryujinx.Horizon/Arp/Ipc/Writer.cs
new file mode 100644
index 0000000000..29c98b7798
--- /dev/null
+++ b/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;
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs b/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs
index c165f46fae..4ed7dd48e2 100644
--- a/src/Ryujinx.Horizon/Prepo/Ipc/PrepoService.cs
+++ b/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}");
diff --git a/src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs b/src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs
index fd3f86ff98..1902cde23a 100644
--- a/src/Ryujinx.Horizon/Prepo/PrepoIpcServer.cs
+++ b/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();
         }
     }
diff --git a/src/Ryujinx.Horizon/Prepo/PrepoServerManager.cs b/src/Ryujinx.Horizon/Prepo/PrepoServerManager.cs
index 0c1c782f63..8cac44c8ff 100644
--- a/src/Ryujinx.Horizon/Prepo/PrepoServerManager.cs
+++ b/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
             };
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ApplicationCertificate.cs b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationCertificate.cs
new file mode 100644
index 0000000000..d60d337d3d
--- /dev/null
+++ b/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
+    {
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ApplicationKind.cs b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationKind.cs
new file mode 100644
index 0000000000..586e6a98a5
--- /dev/null
+++ b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationKind.cs
@@ -0,0 +1,8 @@
+namespace Ryujinx.Horizon.Sdk.Arp
+{
+    public enum ApplicationKind : byte
+    {
+        Application,
+        MicroApplication,
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ApplicationLaunchProperty.cs b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationLaunchProperty.cs
new file mode 100644
index 0000000000..00b8183216
--- /dev/null
+++ b/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;
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ApplicationProcessProperty.cs b/src/Ryujinx.Horizon/Sdk/Arp/ApplicationProcessProperty.cs
new file mode 100644
index 0000000000..13d222a127
--- /dev/null
+++ b/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;
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ArpApi.cs b/src/Ryujinx.Horizon/Sdk/Arp/ArpApi.cs
new file mode 100644
index 0000000000..b0acc0062f
--- /dev/null
+++ b/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);
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/ArpResult.cs b/src/Ryujinx.Horizon/Sdk/Arp/ArpResult.cs
new file mode 100644
index 0000000000..5de07871de
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstance.cs b/src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstance.cs
new file mode 100644
index 0000000000..5eb0ab1842
--- /dev/null
+++ b/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; }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstanceManager.cs b/src/Ryujinx.Horizon/Sdk/Arp/Detail/ApplicationInstanceManager.cs
new file mode 100644
index 0000000000..18c993ce4d
--- /dev/null
+++ b/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);
+            }
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/IReader.cs b/src/Ryujinx.Horizon/Sdk/Arp/IReader.cs
new file mode 100644
index 0000000000..ef78f7fd6c
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/IRegistrar.cs b/src/Ryujinx.Horizon/Sdk/Arp/IRegistrar.cs
new file mode 100644
index 0000000000..467f3dbd30
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/IUnregistrationNotifier.cs b/src/Ryujinx.Horizon/Sdk/Arp/IUnregistrationNotifier.cs
new file mode 100644
index 0000000000..24b9807d8e
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/IUpdater.cs b/src/Ryujinx.Horizon/Sdk/Arp/IUpdater.cs
new file mode 100644
index 0000000000..f9beeb690e
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Arp/IWriter.cs b/src/Ryujinx.Horizon/Sdk/Arp/IWriter.cs
new file mode 100644
index 0000000000..b3e000e1e9
--- /dev/null
+++ b/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);
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ncm/ApplicationId.cs b/src/Ryujinx.Horizon/Sdk/Ncm/ApplicationId.cs
index 4c5e76e6f2..24b7d9cab4 100644
--- a/src/Ryujinx.Horizon/Sdk/Ncm/ApplicationId.cs
+++ b/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;
 
diff --git a/src/Ryujinx.Horizon/Sdk/Ncm/StorageId.cs b/src/Ryujinx.Horizon/Sdk/Ncm/StorageId.cs
new file mode 100644
index 0000000000..e2fb325056
--- /dev/null
+++ b/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,
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs b/src/Ryujinx.Horizon/Sdk/Ns/ApplicationControlProperty.cs
new file mode 100644
index 0000000000..12c19168dd
--- /dev/null
+++ b/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,
+        }
+    }
+}
diff --git a/src/Ryujinx.Horizon/Sdk/ServiceUtil.cs b/src/Ryujinx.Horizon/Sdk/ServiceUtil.cs
index ccd6c93a63..5527c1e35f 100644
--- a/src/Ryujinx.Horizon/Sdk/ServiceUtil.cs
+++ b/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;
+        }
     }
 }
diff --git a/src/Ryujinx.Horizon/Sdk/Sf/Cmif/CmifRequest.cs b/src/Ryujinx.Horizon/Sdk/Sf/Cmif/CmifRequest.cs
index d409be5b3e..62c15baa68 100644
--- a/src/Ryujinx.Horizon/Sdk/Sf/Cmif/CmifRequest.cs
+++ b/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;
     }
 }
diff --git a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcBufferDescriptor.cs b/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcBufferDescriptor.cs
index 03ef6d3fc1..4e96289474 100644
--- a/src/Ryujinx.Horizon/Sdk/Sf/Hipc/HipcBufferDescriptor.cs
+++ b/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);
+        }
     }
 }
diff --git a/src/Ryujinx.Horizon/ServiceTable.cs b/src/Ryujinx.Horizon/ServiceTable.cs
index c79328a961..ee62ee84d4 100644
--- a/src/Ryujinx.Horizon/ServiceTable.cs
+++ b/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>();