From 4d5697892e70f1d508ae6dbddf1e4b518c97fd84 Mon Sep 17 00:00:00 2001 From: T <90155059+TarkusR@users.noreply.github.com> Date: Sat, 10 Oct 2026 14:44:13 +0200 Subject: [PATCH] [HLE] Savedata and np fixes (#1080) * [SaveData] Read the dialog mode and user data at their offsets in the open parameters * [SaveData] Report each slot's own block count instead of the fixed maximum * [Np] Report the offline profile as signed out from sceNpCheckNpReachability --- src/SharpEmu.Libs/Np/NpManagerExports.cs | 19 +++++-- .../SaveData/SaveDataDialogExports.cs | 4 +- src/SharpEmu.Libs/SaveData/SaveDataExports.cs | 44 ++++++++++++---- src/SharpEmu.Libs/SaveData/SaveDataStorage.cs | 3 ++ .../Np/NpManagerExportsTests.cs | 50 +++++++++++++++++++ .../SaveData/SaveDataExportsTests.cs | 8 +-- 6 files changed, 109 insertions(+), 19 deletions(-) create mode 100644 tests/SharpEmu.Libs.Tests/Np/NpManagerExportsTests.cs diff --git a/src/SharpEmu.Libs/Np/NpManagerExports.cs b/src/SharpEmu.Libs/Np/NpManagerExports.cs index 08c237f..a7da637 100644 --- a/src/SharpEmu.Libs/Np/NpManagerExports.cs +++ b/src/SharpEmu.Libs/Np/NpManagerExports.cs @@ -3,6 +3,7 @@ using SharpEmu.HLE; using System.Buffers.Binary; +using System.Collections.Concurrent; namespace SharpEmu.Libs.Np; @@ -11,7 +12,9 @@ public static class NpManagerExports private const int NpTitleIdSize = 16; private const int NpTitleSecretSize = 128; private const int NpErrorInvalidArgument = unchecked((int)0x80550003); + private const int NpErrorSignedOut = unchecked((int)0x80550006); private static int _nextNpRequestId; + private static readonly ConcurrentDictionary _requestResults = new(); [SysAbiExport( Nid = "3Zl8BePTh9Y", @@ -28,8 +31,17 @@ public static class NpManagerExports public static int NpCreateAsyncRequest(CpuContext ctx) => SetReturn(ctx, NextRequestId()); [SysAbiExport(Nid = "KfGZg2y73oM", ExportName = "sceNpCheckNpReachability", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")] - public static int NpCheckNpReachability(CpuContext ctx) => - SetReturn(ctx, unchecked((int)ctx[CpuRegister.Rdi]) > 0 ? (int)OrbisGen2Result.ORBIS_GEN2_OK : NpErrorInvalidArgument); + public static int NpCheckNpReachability(CpuContext ctx) + { + var requestId = unchecked((int)ctx[CpuRegister.Rdi]); + if (requestId <= 0) + { + return SetReturn(ctx, NpErrorInvalidArgument); + } + + _requestResults[requestId] = NpErrorSignedOut; + return SetReturn(ctx, NpErrorSignedOut); + } [SysAbiExport(Nid = "GpLQDNKICac", ExportName = "sceNpCreateRequest", Target = Generation.Gen4 | Generation.Gen5, LibraryName = "libSceNpManager")] public static int NpCreateRequest(CpuContext ctx) => SetReturn(ctx, NextRequestId()); @@ -50,7 +62,7 @@ public static class NpManagerExports if (resultAddress != 0) { Span result = stackalloc byte[sizeof(int)]; - BinaryPrimitives.WriteInt32LittleEndian(result, 0); + BinaryPrimitives.WriteInt32LittleEndian(result, _requestResults.GetValueOrDefault(requestId)); if (!ctx.Memory.TryWrite(resultAddress, result)) { return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); @@ -73,6 +85,7 @@ public static class NpManagerExports LibraryName = "libSceNpManager")] public static int NpDeleteRequest(CpuContext ctx) { + _requestResults.TryRemove(unchecked((int)ctx[CpuRegister.Rdi]), out _); ctx[CpuRegister.Rax] = 0; return (int)OrbisGen2Result.ORBIS_GEN2_OK; } diff --git a/src/SharpEmu.Libs/SaveData/SaveDataDialogExports.cs b/src/SharpEmu.Libs/SaveData/SaveDataDialogExports.cs index 85d150c..cb718b2 100644 --- a/src/SharpEmu.Libs/SaveData/SaveDataDialogExports.cs +++ b/src/SharpEmu.Libs/SaveData/SaveDataDialogExports.cs @@ -64,8 +64,8 @@ public static class SaveDataDialogExports return ctx.SetReturn(ErrorNotInitialized); } - _lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0; - _lastUserData = ctx.TryReadUInt64(paramAddress + 0xC8, out var userData) ? userData : 0; + _lastMode = TryReadInt32(ctx, paramAddress + 0x34, out var mode) ? mode : 0; + _lastUserData = ctx.TryReadUInt64(paramAddress + 0x70, out var userData) ? userData : 0; // There is no host save dialog yet. Enter RUNNING so the close path sees a live // dialog; the guest's next status poll auto-dismisses it (see PollStatus). diff --git a/src/SharpEmu.Libs/SaveData/SaveDataExports.cs b/src/SharpEmu.Libs/SaveData/SaveDataExports.cs index 4bde666..3eac9da 100644 --- a/src/SharpEmu.Libs/SaveData/SaveDataExports.cs +++ b/src/SharpEmu.Libs/SaveData/SaveDataExports.cs @@ -81,6 +81,7 @@ public static class SaveDataExports private const int MountInfoSize = 0x30; private const uint SaveDataBlockSize = 65536; private const ulong SaveDataBlocksMax = 16384; + private const ulong SaveDataBlocksMin = 48; private static readonly object _eventGate = new(); private static readonly Queue _events = new(); @@ -94,7 +95,7 @@ public static class SaveDataExports int UserId, string TitleId, string DirName); - private sealed record MountEntry(string SlotDir, string DirName, int UserId); + private sealed record MountEntry(string SlotDir, string DirName, int UserId, ulong Blocks); private static void EnqueueEvent( uint type, @@ -304,13 +305,13 @@ public static class SaveDataExports } var usedBlocks = GetUsedBlocks(SafeDirectorySize(entry.SlotDir)); - var freeBlocks = GetFreeBlocks(usedBlocks); + var freeBlocks = GetFreeBlocks(entry.Blocks, usedBlocks); Span info = stackalloc byte[MountInfoSize]; info.Clear(); - BinaryPrimitives.WriteUInt64LittleEndian(info[0x00..], SaveDataBlocksMax); + BinaryPrimitives.WriteUInt64LittleEndian(info[0x00..], entry.Blocks); BinaryPrimitives.WriteUInt64LittleEndian(info[0x08..], freeBlocks); TraceSaveData( - $"get_mount_info mount='{mountPoint}' blocks={SaveDataBlocksMax} " + + $"get_mount_info mount='{mountPoint}' blocks={entry.Blocks} " + $"used_blocks={usedBlocks} free_blocks={freeBlocks}"); return ctx.Memory.TryWrite(infoAddress, info) ? SetReturn(ctx, 0) @@ -395,7 +396,7 @@ public static class SaveDataExports return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); } - var metadata = new SaveDataMetadata + var metadata = SaveDataStorage.ReadMetadata(entry.SlotDir) with { Title = ReadAsciiField(raw.Slice(0x00, 128)), SubTitle = ReadAsciiField(raw.Slice(0x80, 128)), @@ -864,6 +865,7 @@ public static class SaveDataExports Directory.CreateDirectory(savePath); } + var slotBlocks = ResolveSlotBlocks(savePath, blocks); string mountPoint; lock (_mountGate) { @@ -882,7 +884,7 @@ public static class SaveDataExports } mountPoint = $"/savedata{slot}"; - _mounts[mountPoint] = new MountEntry(savePath, dirName, userId); + _mounts[mountPoint] = new MountEntry(savePath, dirName, userId, slotBlocks); } KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, savePath); @@ -1147,13 +1149,35 @@ public static class SaveDataExports private static bool TryWriteSearchInfo(CpuContext ctx, ulong address, SaveEntry entry) { var usedBlocks = GetUsedBlocks(GetDirectorySize(entry.Path)); + var blocks = SlotBlocks(entry.Path, usedBlocks); Span info = stackalloc byte[SaveDataSearchInfoSize]; info.Clear(); - BinaryPrimitives.WriteUInt64LittleEndian(info[0x00..], SaveDataBlocksMax); - BinaryPrimitives.WriteUInt64LittleEndian(info[0x08..], GetFreeBlocks(usedBlocks)); + BinaryPrimitives.WriteUInt64LittleEndian(info[0x00..], blocks); + BinaryPrimitives.WriteUInt64LittleEndian(info[0x08..], GetFreeBlocks(blocks, usedBlocks)); return ctx.Memory.TryWrite(address, info); } + private static ulong ClampBlocks(ulong blocks) => Math.Clamp(blocks, SaveDataBlocksMin, SaveDataBlocksMax); + + private static ulong SlotBlocks(string slotDir, ulong usedBlocks) + { + var blocks = SaveDataStorage.ReadMetadata(slotDir).Blocks; + return blocks != 0 ? blocks : ClampBlocks(usedBlocks); + } + + private static ulong ResolveSlotBlocks(string slotDir, ulong requestedBlocks) + { + var metadata = SaveDataStorage.ReadMetadata(slotDir); + if (metadata.Blocks != 0) + { + return metadata.Blocks; + } + + var blocks = ClampBlocks(Math.Max(requestedBlocks, GetUsedBlocks(SafeDirectorySize(slotDir)))); + SaveDataStorage.WriteMetadata(slotDir, metadata with { Blocks = blocks }); + return blocks; + } + private static ulong GetUsedBlocks(long byteCount) { if (byteCount <= 0) @@ -1164,8 +1188,8 @@ public static class SaveDataExports return checked(((ulong)byteCount + SaveDataBlockSize - 1) / SaveDataBlockSize); } - private static ulong GetFreeBlocks(ulong usedBlocks) => - usedBlocks >= SaveDataBlocksMax ? 0 : SaveDataBlocksMax - usedBlocks; + private static ulong GetFreeBlocks(ulong blocks, ulong usedBlocks) => + usedBlocks >= blocks ? 0 : blocks - usedBlocks; private static long GetDirectorySize(string root) { diff --git a/src/SharpEmu.Libs/SaveData/SaveDataStorage.cs b/src/SharpEmu.Libs/SaveData/SaveDataStorage.cs index b27ed10..ec92bb8 100644 --- a/src/SharpEmu.Libs/SaveData/SaveDataStorage.cs +++ b/src/SharpEmu.Libs/SaveData/SaveDataStorage.cs @@ -174,6 +174,9 @@ public sealed record SaveDataMetadata [JsonPropertyName("userParam")] public uint UserParam { get; init; } + [JsonPropertyName("blocks")] + public ulong Blocks { get; init; } + public static SaveDataMetadata CreateDefault(string dirName) => new() { Title = string.IsNullOrWhiteSpace(dirName) ? "Saved Data" : dirName }; } diff --git a/tests/SharpEmu.Libs.Tests/Np/NpManagerExportsTests.cs b/tests/SharpEmu.Libs.Tests/Np/NpManagerExportsTests.cs new file mode 100644 index 0000000..7f68a07 --- /dev/null +++ b/tests/SharpEmu.Libs.Tests/Np/NpManagerExportsTests.cs @@ -0,0 +1,50 @@ +// Copyright (C) 2026 SharpEmu Emulator Project +// SPDX-License-Identifier: GPL-2.0-or-later + +using System.Buffers.Binary; +using SharpEmu.HLE; +using SharpEmu.Libs.Np; +using Xunit; + +namespace SharpEmu.Libs.Tests.Np; + +public sealed class NpManagerExportsTests +{ + private const ulong MemoryBase = 0x1_0000_0000; + + [Fact] + public void CheckNpReachability_ReportsSignedOutDirectlyAndThroughPolling() + { + var memory = new FakeCpuMemory(MemoryBase, 0x1000); + var ctx = new CpuContext(memory, Generation.Gen5); + var requestId = NpManagerExports.NpCreateAsyncRequest(ctx); + + ctx[CpuRegister.Rdi] = unchecked((uint)requestId); + ctx[CpuRegister.Rsi] = 1; + Assert.Equal(unchecked((int)0x80550006), NpManagerExports.NpCheckNpReachability(ctx)); + + var resultAddress = MemoryBase + 0x100; + ctx[CpuRegister.Rdi] = unchecked((uint)requestId); + ctx[CpuRegister.Rsi] = resultAddress; + Assert.Equal(0, NpManagerExports.NpPollAsync(ctx)); + Span resultBytes = stackalloc byte[4]; + Assert.True(memory.TryRead(resultAddress, resultBytes)); + Assert.Equal(unchecked((int)0x80550006), BinaryPrimitives.ReadInt32LittleEndian(resultBytes)); + + ctx[CpuRegister.Rdi] = unchecked((uint)requestId); + Assert.Equal(0, NpManagerExports.NpDeleteRequest(ctx)); + ctx[CpuRegister.Rdi] = unchecked((uint)requestId); + ctx[CpuRegister.Rsi] = resultAddress; + Assert.Equal(0, NpManagerExports.NpPollAsync(ctx)); + Assert.True(memory.TryRead(resultAddress, resultBytes)); + Assert.Equal(0, BinaryPrimitives.ReadInt32LittleEndian(resultBytes)); + } + + [Fact] + public void CheckNpReachability_RejectsAnInvalidRequest() + { + var ctx = new CpuContext(new FakeCpuMemory(MemoryBase, 0x1000), Generation.Gen5); + ctx[CpuRegister.Rdi] = 0; + Assert.Equal(unchecked((int)0x80550003), NpManagerExports.NpCheckNpReachability(ctx)); + } +} diff --git a/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs b/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs index 93f9147..003dae6 100644 --- a/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs +++ b/tests/SharpEmu.Libs.Tests/SaveData/SaveDataExportsTests.cs @@ -243,8 +243,8 @@ public sealed class SaveDataExportsTests : IDisposable Reg(rdi: MountPointStr, rsi: MountInfo))); Assert.True(_ctx.TryReadUInt64(MountInfo, out var blocks)); Assert.True(_ctx.TryReadUInt64(MountInfo + 0x08, out var freeBlocks)); - Assert.Equal(16384UL, blocks); - Assert.Equal(16382UL, freeBlocks); + Assert.Equal(48UL, blocks); + Assert.Equal(46UL, freeBlocks); } [Fact] @@ -290,8 +290,8 @@ public sealed class SaveDataExportsTests : IDisposable Reg(rdi: SearchCond, rsi: SearchResult))); Assert.True(_ctx.TryReadUInt64(SearchInfos, out var blocks)); Assert.True(_ctx.TryReadUInt64(SearchInfos + 0x08, out var freeBlocks)); - Assert.Equal(16384UL, blocks); - Assert.Equal(16382UL, freeBlocks); + Assert.Equal(48UL, blocks); + Assert.Equal(46UL, freeBlocks); } [Fact]