[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:
parent
46a7d3a446
commit
4d5697892e
|
|
@ -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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
{
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
@ -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]
|
||||
|
|
|
|||
Loading…
Reference in New Issue