[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
This commit is contained in:
T 2026-10-10 14:44:13 +02:00 committed by GitHub
parent 46a7d3a446
commit 4d5697892e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 109 additions and 19 deletions

View File

@ -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<int, int> _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<byte> 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;
}

View File

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

View File

@ -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<SaveDataEvent> _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<byte> 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<byte> 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)
{

View File

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

View File

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

View File

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