mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-23 03:16:22 +08:00
[SaveData] Implement save data memory2 exports (#297)
* [SaveData] Implement save data memory2 exports Astro Bot calls sceSaveDataSetupSaveDataMemory2 during boot and asserts and null-writes when it fails, so the missing import surfaces as a named crash. This implements setup plus the companion get, set, and sync operations that make it useful. The store is one zero-filled file per user and title at sce_sdmemory/memory.dat under the save root, readiness is the backing file's existence, and get, set, and sync return MEMORY_NOT_READY before setup. Struct offsets follow the publicly documented homebrew savedata headers. * [SaveData] Write setup result before mutating the memory backing file
This commit is contained in:
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
@@ -14,6 +15,7 @@ public static class SaveDataExports
|
||||
private const int OrbisSaveDataErrorExists = unchecked((int)0x809F0007);
|
||||
private const int OrbisSaveDataErrorNotFound = unchecked((int)0x809F0008);
|
||||
private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B);
|
||||
private const int OrbisSaveDataErrorMemoryNotReady = unchecked((int)0x809F0012);
|
||||
private const int SaveDataTitleIdSize = 10;
|
||||
private const int SaveDataDirNameSize = 32;
|
||||
private const int SaveDataParamSize = 0x530;
|
||||
@@ -29,7 +31,10 @@ public static class SaveDataExports
|
||||
private const uint MountModeCreate = 1u << 2;
|
||||
private const uint MountModeCreate2 = 1u << 5;
|
||||
private const int MountResultSize = 0x40;
|
||||
// Emulator guard against corrupt or misread sizes, not a platform limit.
|
||||
private const ulong SaveDataMemoryMaxSize = 64UL * 1024 * 1024;
|
||||
private static readonly object _stateGate = new();
|
||||
private static readonly object _memoryGate = new();
|
||||
private static readonly HashSet<int> _preparedTransactionResources = [];
|
||||
private static string? _titleId;
|
||||
|
||||
@@ -472,6 +477,19 @@ public static class SaveDataExports
|
||||
private static string ResolveTitleSaveRoot(int userId, string titleId) =>
|
||||
Path.Combine(ResolveSaveDataRoot(), userId.ToString(), SanitizePathSegment(titleId));
|
||||
|
||||
private static string ResolveSaveDataMemoryPath(int userId) =>
|
||||
Path.Combine(ResolveTitleSaveRoot(userId, ResolveConfiguredTitleId()), "sce_sdmemory", "memory.dat");
|
||||
|
||||
private static bool TryReadMemoryData(
|
||||
CpuContext ctx, ulong address, out ulong buffer, out ulong size, out ulong offset)
|
||||
{
|
||||
size = 0;
|
||||
offset = 0;
|
||||
return ctx.TryReadUInt64(address, out buffer) &&
|
||||
ctx.TryReadUInt64(address + 0x08, out size) &&
|
||||
ctx.TryReadUInt64(address + 0x10, out offset);
|
||||
}
|
||||
|
||||
private static string ResolveSaveDataRoot()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
||||
@@ -670,4 +688,197 @@ public static class SaveDataExports
|
||||
TraceSaveData($"commit commit=0x{commitAddress:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
// Save data memory: a small per-user blob titles read and write without
|
||||
// mounting anything, backed by one zero-filled file per user and title.
|
||||
[SysAbiExport(
|
||||
Nid = "oQySEUfgXRA",
|
||||
ExportName = "sceSaveDataSetupSaveDataMemory2",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataSetupSaveDataMemory2(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, paramAddress + 0x04, out var userId) ||
|
||||
!ctx.TryReadUInt64(paramAddress + 0x08, out var memorySize))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0 || memorySize == 0 || memorySize > SaveDataMemoryMaxSize)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = ResolveSaveDataMemoryPath(userId);
|
||||
lock (_memoryGate)
|
||||
{
|
||||
var backing = new FileInfo(path);
|
||||
var existedSize = backing.Exists ? (ulong)backing.Length : 0;
|
||||
|
||||
// The result write comes first so a faulted result pointer
|
||||
// cannot leave created or grown setup state behind.
|
||||
if (resultAddress != 0 && !ctx.TryWriteUInt64(resultAddress, existedSize))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (existedSize < memorySize)
|
||||
{
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(path)!);
|
||||
using var stream = new FileStream(path, FileMode.OpenOrCreate, FileAccess.ReadWrite);
|
||||
stream.SetLength((long)memorySize);
|
||||
}
|
||||
|
||||
TraceSaveData($"memory-setup2 user={userId} size=0x{memorySize:X} existed=0x{existedSize:X}");
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QwOO7vegnV8",
|
||||
ExportName = "sceSaveDataGetSaveDataMemory2",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataGetSaveDataMemory2(CpuContext ctx) =>
|
||||
TransferSaveDataMemory(ctx, write: false);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "cduy9v4YmT4",
|
||||
ExportName = "sceSaveDataSetSaveDataMemory2",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataSetSaveDataMemory2(CpuContext ctx) =>
|
||||
TransferSaveDataMemory(ctx, write: true);
|
||||
|
||||
// Writes go straight through to the backing file, so a ready state is
|
||||
// all sync has to confirm.
|
||||
[SysAbiExport(
|
||||
Nid = "wiT9jeC7xPw",
|
||||
ExportName = "sceSaveDataSyncSaveDataMemory",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataSyncSaveDataMemory(CpuContext ctx)
|
||||
{
|
||||
var syncAddress = ctx[CpuRegister.Rdi];
|
||||
if (syncAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, syncAddress, out var userId))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(
|
||||
File.Exists(ResolveSaveDataMemoryPath(userId)) ? 0 : OrbisSaveDataErrorMemoryNotReady);
|
||||
}
|
||||
|
||||
private static int TransferSaveDataMemory(CpuContext ctx, bool write)
|
||||
{
|
||||
var requestAddress = ctx[CpuRegister.Rdi];
|
||||
if (requestAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, requestAddress, out var userId) ||
|
||||
!ctx.TryReadUInt64(requestAddress + 0x08, out var dataAddress))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var path = ResolveSaveDataMemoryPath(userId);
|
||||
lock (_memoryGate)
|
||||
{
|
||||
if (!File.Exists(path))
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorMemoryNotReady);
|
||||
}
|
||||
|
||||
if (dataAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
if (!TryReadMemoryData(ctx, dataAddress, out var bufAddress, out var bufSize, out var offset))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
using var stream = new FileStream(
|
||||
path, FileMode.Open, write ? FileAccess.ReadWrite : FileAccess.Read);
|
||||
var length = (ulong)stream.Length;
|
||||
if (bufAddress == 0 || bufSize > length || offset > length - bufSize)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
// The guarded file length bounds bufSize, so one rented buffer
|
||||
// covers the transfer and a guest fault never partially writes.
|
||||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Max(bufSize, 1));
|
||||
try
|
||||
{
|
||||
var span = buffer.AsSpan(0, (int)bufSize);
|
||||
stream.Seek((long)offset, SeekOrigin.Begin);
|
||||
if (write)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(bufAddress, span))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
stream.Write(span);
|
||||
}
|
||||
else
|
||||
{
|
||||
stream.ReadExactly(span);
|
||||
if (!ctx.Memory.TryWrite(bufAddress, span))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
TraceSaveData(
|
||||
$"memory-{(write ? "set2" : "get2")} user={userId} offset=0x{offset:X} size=0x{bufSize:X}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user