Add SaveData transaction and NP UDS layout HLE stubs (#168)

* Add SaveData transaction and NP UDS layout HLE stubs

Wire Prepare, Commit, and Umount2 for implicit save transactions,
unregister guest mounts on Umount2, and add NP UDS CreateEvent,
DestroyEvent, and EventPropertyObjectSetString for layout-load imports.

* Add NP UDS SetArray and PostEvent layout HLE stubs

Add sceNpUniversalDataSystemEventPropertyObjectSetArray and
sceNpUniversalDataSystemPostEvent for layout-load imports on PPSA02929.
This commit is contained in:
Mike Saito
2026-07-15 01:34:59 +03:00
committed by GitHub
parent 5e2c21edf1
commit ae5ef0abe7
3 changed files with 239 additions and 0 deletions
@@ -200,6 +200,40 @@ public static class KernelMemoryCompatExports
}
}
public static bool TryUnregisterGuestPathMount(string guestMountPoint)
{
if (string.IsNullOrWhiteSpace(guestMountPoint))
{
return false;
}
var normalizedMountPoint = NormalizeGuestStatCachePath(guestMountPoint);
if (normalizedMountPoint is null || normalizedMountPoint == "/")
{
return false;
}
var removed = false;
lock (_guestMountGate)
{
removed = _guestMounts.Remove(normalizedMountPoint);
}
if (!removed)
{
return false;
}
lock (_statCacheGate)
{
_negativeStatCache.RemoveWhere(path =>
string.Equals(path, normalizedMountPoint, StringComparison.OrdinalIgnoreCase) ||
path.StartsWith(normalizedMountPoint + "/", StringComparison.OrdinalIgnoreCase));
}
return true;
}
internal static bool TryAllocateHleData(
CpuContext ctx,
ulong length,
@@ -9,7 +9,10 @@ namespace SharpEmu.Libs.Np;
public static class NpUniversalDataSystemExports
{
private const int NpUniversalDataSystemErrorInvalidArgument = unchecked((int)0x80553102);
private static readonly object _eventGate = new();
private static readonly HashSet<int> _createdEvents = [];
private static int _nextHandle = 1;
private static int _nextEvent = 1;
[SysAbiExport(
Nid = "sjaobBgqeB4",
@@ -67,6 +70,114 @@ public static class NpUniversalDataSystemExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
}
[SysAbiExport(
Nid = "p+GcLqwpL9M",
ExportName = "sceNpUniversalDataSystemCreateEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemCreateEvent(CpuContext ctx)
{
var parameterAddress = ctx[CpuRegister.Rdi];
if (parameterAddress == 0)
{
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
}
var eventId = Interlocked.Increment(ref _nextEvent);
lock (_eventGate)
{
_createdEvents.Add(eventId);
}
if (ctx.TryWriteInt32(ctx[CpuRegister.Rdx], eventId, checkNil: true) ||
ctx.TryWriteInt32(ctx[CpuRegister.Rcx], eventId, checkNil: true))
{
return ctx.SetReturn(0, typeof(long));
}
lock (_eventGate)
{
_createdEvents.Remove(eventId);
}
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
}
[SysAbiExport(
Nid = "wG+84pnNIuo",
ExportName = "sceNpUniversalDataSystemDestroyEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemDestroyEvent(CpuContext ctx)
{
var eventId = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_eventGate)
{
_createdEvents.Remove(eventId);
}
return ctx.SetReturn(0, typeof(long));
}
[SysAbiExport(
Nid = "MfDb+4Nln64",
ExportName = "sceNpUniversalDataSystemEventPropertyObjectSetString",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemEventPropertyObjectSetString(CpuContext ctx)
{
var propertyObjectAddress = ctx[CpuRegister.Rsi];
var valueAddress = ctx[CpuRegister.Rdx];
if (propertyObjectAddress == 0 || valueAddress == 0)
{
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
}
Span<byte> probe = stackalloc byte[1];
return ctx.Memory.TryRead(propertyObjectAddress, probe) &&
ctx.Memory.TryRead(valueAddress, probe)
? ctx.SetReturn(0, typeof(long))
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
}
[SysAbiExport(
Nid = "Wxbg5x3pTXA",
ExportName = "sceNpUniversalDataSystemEventPropertyObjectSetArray",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemEventPropertyObjectSetArray(CpuContext ctx)
{
var propertyObjectAddress = ctx[CpuRegister.Rsi];
var valueAddress = ctx[CpuRegister.Rdx];
if (propertyObjectAddress == 0)
{
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
}
Span<byte> probe = stackalloc byte[1];
if (!ctx.Memory.TryRead(propertyObjectAddress, probe))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
}
if (valueAddress != 0 && !ctx.Memory.TryRead(valueAddress, probe))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
}
return ctx.SetReturn(0, typeof(long));
}
[SysAbiExport(
Nid = "CzkKf7ahIyU",
ExportName = "sceNpUniversalDataSystemPostEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemPostEvent(CpuContext ctx)
{
return ctx.SetReturn(0, typeof(long));
}
[SysAbiExport(
Nid = "tpFJ8LIKvPw",
ExportName = "sceNpUniversalDataSystemRegisterContext",
@@ -31,6 +31,7 @@ public static class SaveDataExports
private const int MountResultSize = 0x40;
private static readonly object _stateGate = new();
private static readonly HashSet<int> _transactionResources = [];
private static readonly HashSet<int> _preparedTransactionResources = [];
private static string? _titleId;
private static int _nextTransactionResource;
@@ -40,6 +41,7 @@ public static class SaveDataExports
{
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
_transactionResources.Clear();
_preparedTransactionResources.Clear();
_nextTransactionResource = 0;
}
}
@@ -278,12 +280,104 @@ public static class SaveDataExports
lock (_stateGate)
{
_transactionResources.Remove(resource);
_preparedTransactionResources.Remove(resource);
}
TraceSaveData($"delete_transaction_resource resource={resource}");
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "sDCBrmc61XU",
ExportName = "sceSaveDataPrepare",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataPrepare(CpuContext ctx)
{
var mountPointAddress = ctx[CpuRegister.Rdi];
var resource = unchecked((int)ctx[CpuRegister.Rdx]);
if (mountPointAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (string.IsNullOrWhiteSpace(mountPoint))
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
lock (_stateGate)
{
if (resource != 0)
{
_preparedTransactionResources.Add(resource);
}
}
TraceSaveData($"prepare mount_point={mountPoint} resource={resource}");
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "ie7qhZ4X0Cc",
ExportName = "sceSaveDataCommit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataCommit(CpuContext ctx)
{
var commitAddress = ctx[CpuRegister.Rdi];
if (commitAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
lock (_stateGate)
{
_preparedTransactionResources.Clear();
}
TraceSaveData($"commit commit=0x{commitAddress:X16}");
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "uW4vfTwMQVo",
ExportName = "sceSaveDataUmount2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataUmount2(CpuContext ctx)
{
var mountPointAddress = ctx[CpuRegister.Rdi];
if (mountPointAddress == 0)
{
mountPointAddress = ctx[CpuRegister.Rsi];
}
if (mountPointAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
if (!TryReadFixedAscii(ctx, mountPointAddress, 16, out var mountPoint))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (string.IsNullOrWhiteSpace(mountPoint))
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
var unmounted = KernelMemoryCompatExports.TryUnregisterGuestPathMount(mountPoint);
TraceSaveData($"umount2 mount_point={mountPoint} unregistered={unmounted}");
return ctx.SetReturn(0);
}
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
{
cond = default;