mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 5256ab8dec | |||
| 0d89b2488b | |||
| c3019b78d6 | |||
| dd5e524879 | |||
| df9d0e6aaf | |||
| 253d0fae3f | |||
| 78d719ef9e | |||
| be3806cdf6 | |||
| dc47deee93 | |||
| 1c838cf8d6 | |||
| 2f269566a5 | |||
| 9d7afef6f6 | |||
| daeff21516 | |||
| 82619deb34 | |||
| 697ad7be80 | |||
| 73c987e49f | |||
| cd79275117 | |||
| b14ecae504 | |||
| 873f473b65 | |||
| 9a1a3789ef | |||
| 0f3d4032cb | |||
| 0c4a757695 | |||
| 5fcc8eaa37 | |||
| 21105e0346 | |||
| b424df5a64 | |||
| 7f85971e39 | |||
| e28259a99d | |||
| 3d2a4b74ac |
Binary file not shown.
|
After Width: | Height: | Size: 933 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
@@ -9,11 +9,17 @@ on:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "**/*.png"
|
||||
- "**/*.jpeg"
|
||||
- "**/*.jpg"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "**/*.png"
|
||||
- "**/*.jpeg"
|
||||
- "**/*.jpg"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
|
||||
@@ -482,6 +482,8 @@ public sealed partial class DirectExecutionBackend
|
||||
catch (Exception ex)
|
||||
{
|
||||
LastError = $"HLE dispatch error for {importStubEntry.Nid}: {ex.GetType().Name}: {ex.Message}";
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] {LastError}");
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] {ex.StackTrace}");
|
||||
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
||||
return 18446744071562199298uL;
|
||||
}
|
||||
@@ -527,15 +529,14 @@ public sealed partial class DirectExecutionBackend
|
||||
if (dispatchIndex % 100000 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid})");
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid}) " +
|
||||
$"rdi=0x{arg0:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} " +
|
||||
$"rdx=0x{cpuContext[CpuRegister.Rdx]:X16} rcx=0x{cpuContext[CpuRegister.Rcx]:X16} " +
|
||||
$"ret=0x{returnRip:X16}");
|
||||
}
|
||||
|
||||
var previousImportCallFrame = GuestThreadExecution.EnterImportCallFrame(
|
||||
returnRip,
|
||||
(ulong)argPackPtr + 104uL,
|
||||
ActiveGuestReturnSlotAddress);
|
||||
int returnValue;
|
||||
try
|
||||
if (IsNoBlockLeafImport(importStubEntry.Nid))
|
||||
{
|
||||
cpuContext.ClearRaxWriteFlag();
|
||||
returnValue = export.Function(cpuContext);
|
||||
@@ -544,9 +545,25 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
|
||||
}
|
||||
}
|
||||
finally
|
||||
else
|
||||
{
|
||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||
var previousImportCallFrame = GuestThreadExecution.EnterImportCallFrame(
|
||||
returnRip,
|
||||
(ulong)argPackPtr + 104uL,
|
||||
ActiveGuestReturnSlotAddress);
|
||||
try
|
||||
{
|
||||
cpuContext.ClearRaxWriteFlag();
|
||||
returnValue = export.Function(cpuContext);
|
||||
if (!cpuContext.WasRaxWritten)
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||
}
|
||||
}
|
||||
|
||||
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
@@ -591,6 +608,28 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsNoBlockLeafImport(string nid) =>
|
||||
nid is
|
||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||
"a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
|
||||
"Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
|
||||
"GuchCTefuZw" or // sceAmprCommandBufferDestructor
|
||||
"N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
|
||||
"baQO9ez2gL4" or // sceAmprCommandBufferReset
|
||||
"ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
|
||||
"mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
|
||||
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
|
||||
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
|
||||
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
|
||||
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
|
||||
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
|
||||
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
|
||||
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
|
||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||
"qvMUCyyaCSI"; // sceKernelAprSubmitCommandBufferAndGetId
|
||||
|
||||
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
||||
{
|
||||
var expectedFileProbeMiss =
|
||||
@@ -599,7 +638,10 @@ public sealed partial class DirectExecutionBackend
|
||||
var expectedTimedWaitTimeout =
|
||||
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
|
||||
unchecked((int)result) == 60;
|
||||
if (!expectedFileProbeMiss && !expectedTimedWaitTimeout)
|
||||
var expectedMutexTrylockBusy =
|
||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
if (!expectedFileProbeMiss && !expectedTimedWaitTimeout && !expectedMutexTrylockBusy)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
@@ -635,6 +677,31 @@ public sealed partial class DirectExecutionBackend
|
||||
"7H0iTOciTLo" or
|
||||
"2Z+PpY6CaJg" or
|
||||
"8aI7R7WaOlc" or
|
||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||
"qj7QZpgr9Uw" or // Gen5 graphics type-2 packet
|
||||
"LtTouSCZjHM" or // sceAgcCbNop
|
||||
"k3GhuSNmBLU" or // sceAgcCbDispatch
|
||||
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
|
||||
"JrtiDtKeS38" or // sceAgcAcbResetQueue
|
||||
"cFazmnXpJOE" or // sceAgcAcbEventWrite
|
||||
"KT-hTp-Ch14" or // sceAgcAcbAcquireMem
|
||||
"htn36gPnBk4" or // sceAgcAcbWaitRegMem
|
||||
"eZ4+17OQz4Q" or // sceAgcAcbWriteData
|
||||
"j3EtxFkSIhQ" or // sceAgcAcbDispatchIndirect
|
||||
"gSRnr79F8tQ" or // sceAgcDriverSubmitAcb
|
||||
"i1jyy49AjXU" or // sceAgcDcbWriteData
|
||||
"VmW0Tdpy420" or // sceAgcDcbWaitRegMem
|
||||
"WmAc2MEj6Io" or // sceAgcDcbDmaData
|
||||
"rUuVjyR+Rd4" or // sceAgcDcbGetLodStatsGetSize
|
||||
"vuSXe69VILM" or // sceAgcDcbGetLodStats
|
||||
"RmaJwLtc8rY" or // sceAgcDcbSetBaseIndirectArgs
|
||||
"CtB+A9-VxO0" or // sceAgcDcbDispatchIndirect
|
||||
"+kSrjIVxKFE" or // sceAgcDcbPushMarker
|
||||
"H7uZqCoNuWk" or // sceAgcDcbPopMarker
|
||||
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
|
||||
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
|
||||
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
|
||||
"a8uLzYY--tM" or
|
||||
"Qs1xtplKo0U" or
|
||||
"GuchCTefuZw" or
|
||||
@@ -653,7 +720,18 @@ public sealed partial class DirectExecutionBackend
|
||||
"rqwFKI4PAiM" or
|
||||
"eE4Szl8sil8" or
|
||||
"qvMUCyyaCSI" or
|
||||
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
|
||||
"TywrFKCoLGY" or // sceSaveDataInitialize3
|
||||
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
|
||||
"ZP4e7rlzOUk" or // sceSaveDataMount3
|
||||
"ERKzksauAJA" or // sceSaveDataDialogGetStatus
|
||||
"KK3Bdg1RWK0" or // sceSaveDataDialogUpdateStatus
|
||||
"en7gNVnh878" or // sceSaveDataDialogIsReadyToDisplay
|
||||
"jO8DM8oyego" or // sceNpEntitlementAccessInitialize
|
||||
"TFyU+KFBv54" or // sceNpEntitlementAccessGetAddcontEntitlementInfoList
|
||||
"27bAgiJmOh0" or // pthread_cond_timedwait
|
||||
"iQw3iQPhvUQ" or // sceNetCtlCheckCallback
|
||||
"Q2V+iqvjgC0" or // vsnprintf
|
||||
"j4ViWNHEgww" or // strlen
|
||||
"5jNubw4vlAA" or // strnlen
|
||||
"LHMrG7e8G78" or // wcslen
|
||||
|
||||
@@ -347,10 +347,16 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
public string Name { get; init; } = string.Empty;
|
||||
|
||||
public int Priority { get; init; }
|
||||
|
||||
public ulong AffinityMask { get; init; }
|
||||
|
||||
public CpuContext Context { get; init; } = null!;
|
||||
|
||||
public GuestThreadRunState State { get; set; }
|
||||
|
||||
public ulong ExitValue { get; set; }
|
||||
|
||||
public string? BlockReason { get; set; }
|
||||
|
||||
public bool HasBlockedContinuation { get; set; }
|
||||
@@ -388,14 +394,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
private Action? _work;
|
||||
private volatile bool _stopping;
|
||||
|
||||
public GuestContinuationRunner(ulong guestThreadHandle)
|
||||
public GuestContinuationRunner(ulong guestThreadHandle, ThreadPriority priority)
|
||||
{
|
||||
_guestThreadHandle = guestThreadHandle;
|
||||
_thread = new Thread(ThreadMain)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = $"GuestContinuation-{guestThreadHandle:X}",
|
||||
Priority = ThreadPriority.BelowNormal,
|
||||
Priority = priority,
|
||||
};
|
||||
_thread.Start();
|
||||
}
|
||||
@@ -1262,7 +1268,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
libraryName.IndexOf("Kernel", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static bool PreferLleForLibcExport(string exportName)
|
||||
private bool PreferLleForLibcExport(string exportName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(exportName))
|
||||
{
|
||||
@@ -1283,6 +1289,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (IsLibcAllocatorExport(exportName))
|
||||
{
|
||||
return CanUseLleLibcAllocatorFamily();
|
||||
}
|
||||
if (string.Equals(value, "0", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
@@ -1294,6 +1304,51 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return IsSafeLleLibcExport(exportName);
|
||||
}
|
||||
|
||||
private bool CanUseLleLibcAllocatorFamily()
|
||||
{
|
||||
return HasUsableLleLibcExport("gQX+4GDQjpM", "malloc") &&
|
||||
HasUsableLleLibcExport("tIhsqj0qsFE", "free") &&
|
||||
HasUsableLleLibcExport("2X5agFjKxMc", "calloc") &&
|
||||
HasUsableLleLibcExport("Y7aJ1uydPMo", "realloc") &&
|
||||
HasUsableLleLibcExport("Ujf3KzMvRmI", "memalign") &&
|
||||
HasUsableLleLibcExport("2Btkg8k24Zg", "aligned_alloc") &&
|
||||
HasUsableLleLibcExport("cVSk9y8URbc", "posix_memalign");
|
||||
}
|
||||
|
||||
private bool HasUsableLleLibcExport(string nid, string exportName)
|
||||
{
|
||||
if (TryResolveRuntimeSymbolAddress(nid, out var address) && IsDirectImportTargetUsable(address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var candidate in EnumerateRuntimeSymbolCandidates(exportName))
|
||||
{
|
||||
if (TryResolveRuntimeSymbolAddress(candidate, out address) && IsDirectImportTargetUsable(address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsLibcAllocatorExport(string exportName)
|
||||
{
|
||||
return exportName switch
|
||||
{
|
||||
"malloc" or
|
||||
"free" or
|
||||
"calloc" or
|
||||
"realloc" or
|
||||
"memalign" or
|
||||
"aligned_alloc" or
|
||||
"posix_memalign" or
|
||||
"malloc_usable_size" => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsSafeLleLibcExport(string exportName)
|
||||
{
|
||||
return exportName switch
|
||||
@@ -2393,13 +2448,78 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
Interlocked.Increment(ref _readyGuestThreadCount);
|
||||
}
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] Scheduled guest thread '{thread.Name}' handle=0x{thread.ThreadHandle:X16} entry=0x{thread.EntryPoint:X16} arg=0x{thread.Argument:X16}");
|
||||
$"[LOADER][INFO] Scheduled guest thread '{thread.Name}' handle=0x{thread.ThreadHandle:X16} " +
|
||||
$"entry=0x{thread.EntryPoint:X16} arg=0x{thread.Argument:X16} priority={thread.Priority} " +
|
||||
$"host_priority={MapGuestThreadPriority(thread.Priority)} affinity=0x{thread.AffinityMask:X}");
|
||||
Pump(creatorContext, "pthread_create");
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool SupportsGuestContextTransfer => true;
|
||||
|
||||
public bool TryJoinThread(
|
||||
CpuContext callerContext,
|
||||
ulong threadHandle,
|
||||
out ulong returnValue,
|
||||
out string? error)
|
||||
{
|
||||
returnValue = 0;
|
||||
error = null;
|
||||
if (threadHandle == 0)
|
||||
{
|
||||
error = "thread handle is zero";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (threadHandle == GuestThreadExecution.CurrentGuestThreadHandle)
|
||||
{
|
||||
error = "thread cannot join itself";
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!ActiveForcedGuestExit)
|
||||
{
|
||||
Thread? hostThread;
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
if (!_guestThreads.TryGetValue(threadHandle, out var thread))
|
||||
{
|
||||
error = $"unknown guest thread 0x{threadHandle:X16}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (thread.State == GuestThreadRunState.Exited)
|
||||
{
|
||||
returnValue = thread.ExitValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (thread.State == GuestThreadRunState.Faulted)
|
||||
{
|
||||
error =
|
||||
$"guest thread 0x{threadHandle:X16} faulted: " +
|
||||
(thread.BlockReason ?? "unknown error");
|
||||
return false;
|
||||
}
|
||||
|
||||
hostThread = thread.HostThread;
|
||||
}
|
||||
|
||||
if (hostThread is not null &&
|
||||
!ReferenceEquals(hostThread, Thread.CurrentThread))
|
||||
{
|
||||
hostThread.Join(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
error = "guest execution stopped while joining thread";
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Pump(CpuContext callerContext, string reason)
|
||||
{
|
||||
_ = callerContext;
|
||||
@@ -2448,7 +2568,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = $"SharpEmu-{thread.Name}",
|
||||
Priority = ThreadPriority.BelowNormal,
|
||||
Priority = MapGuestThreadPriority(thread.Priority),
|
||||
};
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
@@ -2851,7 +2971,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
if (_guestThreads.TryGetValue(currentGuestThreadHandle, out var guestThread))
|
||||
{
|
||||
runner = guestThread.ContinuationRunner ??= new GuestContinuationRunner(currentGuestThreadHandle);
|
||||
runner = guestThread.ContinuationRunner ??= new GuestContinuationRunner(
|
||||
currentGuestThreadHandle,
|
||||
MapGuestThreadPriority(guestThread.Priority));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2990,6 +3112,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
EntryPoint = request.EntryPoint,
|
||||
Argument = request.Argument,
|
||||
Name = string.IsNullOrWhiteSpace(request.Name) ? $"Thread-{request.ThreadHandle:X}" : request.Name,
|
||||
Priority = request.Priority,
|
||||
AffinityMask = request.AffinityMask,
|
||||
Context = context,
|
||||
State = GuestThreadRunState.Ready,
|
||||
};
|
||||
@@ -3129,11 +3253,77 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
context.TryWriteUInt64(tlsBase + 0x60, tlsBase);
|
||||
}
|
||||
|
||||
private static ThreadPriority MapGuestThreadPriority(int priority)
|
||||
{
|
||||
if (priority <= 478)
|
||||
{
|
||||
return ThreadPriority.Highest;
|
||||
}
|
||||
if (priority >= 733)
|
||||
{
|
||||
return ThreadPriority.Lowest;
|
||||
}
|
||||
|
||||
return ThreadPriority.Normal;
|
||||
}
|
||||
|
||||
private void ApplyGuestThreadAffinity(ulong guestAffinityMask)
|
||||
{
|
||||
var hostAffinityMask = MapGuestThreadAffinity(guestAffinityMask);
|
||||
if (hostAffinityMask == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (SetThreadAffinityMask(GetCurrentThread(), (nuint)hostAffinityMask) == 0 && _logGuestThreads)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Failed to set guest thread affinity guest=0x{guestAffinityMask:X} " +
|
||||
$"host=0x{hostAffinityMask:X} error={Marshal.GetLastWin32Error()}");
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong MapGuestThreadAffinity(ulong guestAffinityMask)
|
||||
{
|
||||
if (guestAffinityMask == 0 || guestAffinityMask == ulong.MaxValue)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var processorCount = Math.Min(Environment.ProcessorCount, 64);
|
||||
if (processorCount == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
ulong hostAffinityMask = 0;
|
||||
for (var guestCpu = 0; guestCpu < 64; guestCpu++)
|
||||
{
|
||||
if ((guestAffinityMask & (1UL << guestCpu)) == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var hostCpu = processorCount < 8
|
||||
? guestCpu % processorCount
|
||||
: processorCount >= 16
|
||||
? guestCpu * 2
|
||||
: guestCpu;
|
||||
if (hostCpu < processorCount)
|
||||
{
|
||||
hostAffinityMask |= 1UL << hostCpu;
|
||||
}
|
||||
}
|
||||
|
||||
return hostAffinityMask;
|
||||
}
|
||||
|
||||
private void RunGuestThread(GuestThreadState thread, string reason)
|
||||
{
|
||||
var previousLastError = LastError;
|
||||
var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(thread.ThreadHandle);
|
||||
var previousGuestThreadState = _activeGuestThreadState;
|
||||
ApplyGuestThreadAffinity(thread.AffinityMask);
|
||||
Volatile.Write(ref thread.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
||||
_activeGuestThreadState = thread;
|
||||
try
|
||||
@@ -3178,11 +3368,23 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
switch (exitReason)
|
||||
{
|
||||
case GuestNativeCallExitReason.Returned:
|
||||
thread.ExitValue = thread.Context[CpuRegister.Rax];
|
||||
thread.State = GuestThreadRunState.Exited;
|
||||
break;
|
||||
case GuestNativeCallExitReason.Blocked:
|
||||
thread.State = GuestThreadRunState.Blocked;
|
||||
thread.BlockReason = blockReason;
|
||||
if (thread.HasBlockedContinuation &&
|
||||
thread.BlockWakeHandler is not null &&
|
||||
thread.BlockWakeHandler())
|
||||
{
|
||||
thread.State = GuestThreadRunState.Ready;
|
||||
thread.BlockReason = null;
|
||||
thread.BlockWakeHandler = null;
|
||||
thread.BlockDeadlineTimestamp = 0;
|
||||
_readyGuestThreads.Enqueue(thread);
|
||||
Interlocked.Increment(ref _readyGuestThreadCount);
|
||||
}
|
||||
break;
|
||||
default:
|
||||
thread.State = GuestThreadRunState.Faulted;
|
||||
@@ -4144,6 +4346,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern uint GetCurrentThreadId();
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern nint GetCurrentThread();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nuint SetThreadAffinityMask(nint hThread, nuint dwThreadAffinityMask);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint OpenThread(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId);
|
||||
|
||||
|
||||
@@ -9,7 +9,7 @@ namespace SharpEmu.Core.Memory;
|
||||
|
||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
||||
{
|
||||
private readonly object _gate = new();
|
||||
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||
private readonly object _guestAllocationGate = new();
|
||||
private readonly List<MemoryRegion> _regions = new();
|
||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||
@@ -78,7 +78,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
{
|
||||
@@ -89,6 +90,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
Protection = protection
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
var allocationKind = executable ? "executable memory" : "data memory";
|
||||
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
||||
@@ -202,7 +207,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
{
|
||||
@@ -213,6 +219,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
Protection = protection
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
var allocationKind = reservedOnly
|
||||
? "reserved data memory (lazy commit)"
|
||||
@@ -315,7 +325,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
{
|
||||
lock (_guestAllocationGate)
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
@@ -324,6 +335,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_regions.Clear();
|
||||
_pageProtections.Clear();
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
_guestAllocationArenaBase = 0;
|
||||
_guestAllocationOffset = 0;
|
||||
@@ -343,7 +358,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var mapEnd = AlignUp(segmentEnd, PageSize);
|
||||
var mapSize = checked(mapEnd - mapStart);
|
||||
|
||||
lock (_gate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
var existingRegion = FindRegion(mapStart, mapSize);
|
||||
if (existingRegion == null)
|
||||
@@ -376,6 +392,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
||||
@@ -425,7 +445,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
||||
for (var i = 0; i < _regions.Count; i++)
|
||||
@@ -440,11 +461,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
lock (_gate)
|
||||
var requiresExclusiveAccess = false;
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
@@ -456,48 +483,55 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
RestorePageProtections(touchedPages);
|
||||
requiresExclusiveAccess = true;
|
||||
break;
|
||||
}
|
||||
|
||||
fixed (byte* destPtr = destination)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
|
||||
if (!requiresExclusiveAccess)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
return TryReadExclusive(virtualAddress, destination);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
lock (_gate)
|
||||
var requiresExclusiveAccess = false;
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
@@ -509,47 +543,148 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
fixed (byte* srcPtr = source)
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* srcPtr = source)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
finally
|
||||
|
||||
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||
if (IsExecutableProtection(oldProtect))
|
||||
{
|
||||
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
||||
}
|
||||
requiresExclusiveAccess = true;
|
||||
break;
|
||||
}
|
||||
|
||||
fixed (byte* srcPtr = source)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
|
||||
if (!requiresExclusiveAccess)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
return TryWriteExclusive(virtualAddress, source);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (!TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
RestorePageProtections(touchedPages);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (!TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
fixed (byte* srcPtr = source)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* srcPtr = source)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||
if (IsExecutableProtection(oldProtect))
|
||||
{
|
||||
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
|
||||
@@ -561,7 +696,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
public void* GetPointer(ulong virtualAddress)
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
@@ -573,11 +709,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAccessible(ulong virtualAddress, ulong size)
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
@@ -588,6 +729,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
private MemoryRegion? FindRegion(ulong address, ulong size)
|
||||
@@ -611,7 +756,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
|
||||
var end = address + size;
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
@@ -622,6 +768,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
|
||||
return overlapEnd != 0;
|
||||
}
|
||||
@@ -707,15 +857,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var endPage = AlignUp(address + size, PageSize);
|
||||
var commitProtection = GetCommitProtection(region);
|
||||
|
||||
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
||||
var pageAddress = startPage;
|
||||
while (pageAddress < endPage)
|
||||
{
|
||||
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
|
||||
? ulong.MaxValue
|
||||
: info.BaseAddress + info.RegionSize;
|
||||
var rangeEnd = Math.Min(endPage, queriedEnd);
|
||||
if (rangeEnd <= pageAddress)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (info.State == MEM_COMMIT)
|
||||
{
|
||||
pageAddress = rangeEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -724,10 +885,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return false;
|
||||
}
|
||||
|
||||
if (VirtualAlloc((void*)pageAddress, (nuint)PageSize, MEM_COMMIT, commitProtection) == null)
|
||||
var commitSize = rangeEnd - pageAddress;
|
||||
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
pageAddress = rangeEnd;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -10,6 +10,7 @@ using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.AppContent;
|
||||
using SharpEmu.Libs.SaveData;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -136,6 +137,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
KernelModuleRegistry.Reset();
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
||||
|
||||
@@ -10,7 +10,9 @@ public readonly record struct GuestThreadStartRequest(
|
||||
ulong EntryPoint,
|
||||
ulong Argument,
|
||||
ulong AttributeAddress,
|
||||
string Name);
|
||||
string Name,
|
||||
int Priority,
|
||||
ulong AffinityMask);
|
||||
|
||||
public readonly record struct GuestThreadSnapshot(
|
||||
ulong ThreadHandle,
|
||||
@@ -27,6 +29,12 @@ public interface IGuestThreadScheduler
|
||||
|
||||
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
||||
|
||||
bool TryJoinThread(
|
||||
CpuContext callerContext,
|
||||
ulong threadHandle,
|
||||
out ulong returnValue,
|
||||
out string? error);
|
||||
|
||||
void Pump(CpuContext callerContext, string reason);
|
||||
|
||||
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
|
||||
|
||||
+1242
-42
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
@@ -25,6 +26,9 @@ public static class AmprExports
|
||||
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
||||
private static readonly bool _traceAmpr =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||
private static readonly bool _traceAmprReads =
|
||||
_traceAmpr ||
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
|
||||
|
||||
private sealed class CommandBufferState
|
||||
{
|
||||
@@ -77,12 +81,7 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var buffer = 0UL;
|
||||
var size = 0UL;
|
||||
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer);
|
||||
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size);
|
||||
|
||||
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0, aux1, clear: false))
|
||||
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer: 0, size: 0, aux0, aux1, clear: false))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -106,8 +105,9 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, 0) ||
|
||||
!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, 0))
|
||||
Span<byte> auxiliaryPointers = stackalloc byte[sizeof(ulong) * 2];
|
||||
auxiliaryPointers.Clear();
|
||||
if (!ctx.Memory.TryWrite(commandBuffer + CommandBufferAux0Offset, auxiliaryPointers))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -181,8 +181,15 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out var buffer) ||
|
||||
!ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out var size) ||
|
||||
Span<byte> bufferPointers = stackalloc byte[sizeof(ulong) * 2];
|
||||
if (!ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, bufferPointers))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var buffer = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers);
|
||||
var size = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers[sizeof(ulong)..]);
|
||||
if (
|
||||
!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -476,20 +483,34 @@ public static class AmprExports
|
||||
ulong aux1,
|
||||
bool clear)
|
||||
{
|
||||
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
|
||||
if (clear)
|
||||
{
|
||||
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
|
||||
header.Clear();
|
||||
if (!ctx.Memory.TryWrite(commandBuffer, header))
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ctx.Memory.TryRead(commandBuffer, header))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferDataOffset..]);
|
||||
size = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferSizeOffset..]);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
|
||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, aux0) &&
|
||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, aux1) &&
|
||||
WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSelfOffset..], commandBuffer);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferDataOffset..], buffer);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSizeOffset..], size);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux0Offset..], aux0);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux1Offset..], aux1);
|
||||
if (!ctx.Memory.TryWrite(commandBuffer, header))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset: 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
||||
@@ -504,6 +525,26 @@ public static class AmprExports
|
||||
return false;
|
||||
}
|
||||
|
||||
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
||||
{
|
||||
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 3];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(pointers, commandBuffer);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(pointers[sizeof(ulong)..], buffer);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(pointers[(sizeof(ulong) * 2)..], size);
|
||||
return ctx.Memory.TryWrite(commandBuffer + CommandBufferSelfOffset, pointers);
|
||||
}
|
||||
|
||||
private static void UpdateCommandBufferState(
|
||||
ulong commandBuffer,
|
||||
ulong buffer,
|
||||
ulong size,
|
||||
ulong writeOffset)
|
||||
{
|
||||
var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
||||
lock (state)
|
||||
{
|
||||
@@ -511,15 +552,6 @@ public static class AmprExports
|
||||
state.Size = size;
|
||||
state.WriteOffset = writeOffset;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
||||
{
|
||||
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
|
||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferDataOffset, buffer) &&
|
||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferSizeOffset, size);
|
||||
}
|
||||
|
||||
private static bool TryGetCommandBufferState(
|
||||
@@ -540,9 +572,11 @@ public static class AmprExports
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer) &&
|
||||
ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size))
|
||||
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 2];
|
||||
if (ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, pointers))
|
||||
{
|
||||
buffer = BinaryPrimitives.ReadUInt64LittleEndian(pointers);
|
||||
size = BinaryPrimitives.ReadUInt64LittleEndian(pointers[sizeof(ulong)..]);
|
||||
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
||||
lock (state)
|
||||
{
|
||||
@@ -595,12 +629,18 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const int ChunkSize = 64 * 1024;
|
||||
var buffer = new byte[(int)Math.Min((ulong)ChunkSize, size)];
|
||||
const int ChunkSize = 1024 * 1024;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ChunkSize, size));
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||
using var stream = new FileStream(
|
||||
hostPath,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite | FileShare.Delete,
|
||||
ChunkSize,
|
||||
FileOptions.SequentialScan);
|
||||
if (fileOffset >= (ulong)stream.Length)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -633,6 +673,10 @@ public static class AmprExports
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -801,7 +845,7 @@ public static class AmprExports
|
||||
string? hostPath,
|
||||
int result)
|
||||
{
|
||||
if (!_traceAmpr)
|
||||
if (!_traceAmprReads)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -204,6 +204,16 @@ public static class KernelExports
|
||||
var nameAddress = ctx[CpuRegister.R8];
|
||||
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
|
||||
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
|
||||
KernelPthreadExtendedCompatExports.GetThreadStartScheduling(
|
||||
ctx,
|
||||
attrAddress,
|
||||
out var priority,
|
||||
out var affinityMask);
|
||||
KernelPthreadExtendedCompatExports.RegisterThreadStart(
|
||||
threadHandle,
|
||||
name,
|
||||
priority,
|
||||
affinityMask);
|
||||
if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, threadHandle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -212,13 +222,22 @@ public static class KernelExports
|
||||
if (ShouldTracePthread())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} name='{name}' -> thread=0x{threadHandle:X16}");
|
||||
$"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} " +
|
||||
$"entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} " +
|
||||
$"name='{name}' priority={priority} affinity=0x{affinityMask:X} -> thread=0x{threadHandle:X16}");
|
||||
}
|
||||
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (scheduler is not null && entryAddress != 0)
|
||||
{
|
||||
var request = new GuestThreadStartRequest(threadHandle, entryAddress, argument, attrAddress, name);
|
||||
var request = new GuestThreadStartRequest(
|
||||
threadHandle,
|
||||
entryAddress,
|
||||
argument,
|
||||
attrAddress,
|
||||
name,
|
||||
priority,
|
||||
affinityMask);
|
||||
if (!scheduler.TryStartThread(ctx, request, out var error))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
@@ -287,10 +306,6 @@ public static class KernelExports
|
||||
{
|
||||
var threadId = ctx[CpuRegister.Rdi];
|
||||
var returnValueAddress = ctx[CpuRegister.Rsi];
|
||||
if (returnValueAddress != 0 && !ctx.TryWriteUInt64(returnValueAddress, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (ShouldTracePthread())
|
||||
{
|
||||
@@ -298,6 +313,30 @@ public static class KernelExports
|
||||
$"[LOADER][TRACE] pthread_join: thread=0x{threadId:X16} retval_out=0x{returnValueAddress:X16}");
|
||||
}
|
||||
|
||||
var returnValue = 0UL;
|
||||
if (GuestThreadExecution.Scheduler is { } scheduler &&
|
||||
!scheduler.TryJoinThread(ctx, threadId, out returnValue, out var error))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] pthread_join: thread=0x{threadId:X16}: {error}");
|
||||
var result = string.Equals(
|
||||
error,
|
||||
"thread cannot join itself",
|
||||
StringComparison.Ordinal)
|
||||
? OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
|
||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
if (returnValueAddress != 0 &&
|
||||
!ctx.TryWriteUInt64(returnValueAddress, returnValue))
|
||||
{
|
||||
ctx[CpuRegister.Rax] =
|
||||
unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -99,10 +99,12 @@ public static class KernelMemoryCompatExports
|
||||
private static readonly object _tlsGate = new();
|
||||
private static readonly object _ioTraceGate = new();
|
||||
private static readonly object _statCacheGate = new();
|
||||
private static readonly object _guestMountGate = new();
|
||||
private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new();
|
||||
private static readonly Dictionary<ulong, LibcHeapAllocation> _libcAllocations = new();
|
||||
private static readonly Dictionary<ulong, MappedRegion> _mappedRegions = new();
|
||||
private static readonly Dictionary<ulong, ulong> _tlsModuleBlocks = new();
|
||||
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
||||
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static long _nextFileDescriptor = 2;
|
||||
@@ -153,6 +155,32 @@ public static class KernelMemoryCompatExports
|
||||
private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, bool IsDirect, ulong DirectStart);
|
||||
private readonly record struct BatchMapEntry(ulong Start, ulong Offset, ulong Length, byte Protection, byte Type, int Operation);
|
||||
|
||||
public static void RegisterGuestPathMount(string guestMountPoint, string hostRoot)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(guestMountPoint);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(hostRoot);
|
||||
|
||||
var normalizedMountPoint = NormalizeGuestStatCachePath(guestMountPoint);
|
||||
if (normalizedMountPoint is null || normalizedMountPoint == "/")
|
||||
{
|
||||
throw new ArgumentException("Guest mount point must name a directory.", nameof(guestMountPoint));
|
||||
}
|
||||
|
||||
var normalizedHostRoot = Path.GetFullPath(hostRoot);
|
||||
Directory.CreateDirectory(normalizedHostRoot);
|
||||
lock (_guestMountGate)
|
||||
{
|
||||
_guestMounts[normalizedMountPoint] = normalizedHostRoot;
|
||||
}
|
||||
|
||||
lock (_statCacheGate)
|
||||
{
|
||||
_negativeStatCache.RemoveWhere(path =>
|
||||
string.Equals(path, normalizedMountPoint, StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith(normalizedMountPoint + "/", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryAllocateHleData(
|
||||
CpuContext ctx,
|
||||
ulong length,
|
||||
@@ -602,9 +630,7 @@ public static class KernelMemoryCompatExports
|
||||
}
|
||||
else
|
||||
{
|
||||
ulong NextGpArg() => vaCursor.NextGpArg();
|
||||
double NextFloatArg() => vaCursor.NextFloatArg();
|
||||
rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
|
||||
rendered = FormatString(ctx, format, ref vaCursor);
|
||||
}
|
||||
|
||||
Console.Write(rendered);
|
||||
@@ -2908,9 +2934,8 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
var format = Encoding.UTF8.GetString(formatBytes);
|
||||
var result = FormatString(ctx, format);
|
||||
var outputBytes = Encoding.UTF8.GetBytes(result);
|
||||
|
||||
return WriteSnprintfOutput(ctx, destination, bufferSize, outputBytes);
|
||||
return WriteSnprintfOutput(ctx, destination, bufferSize, result);
|
||||
}
|
||||
|
||||
private static int VsnprintfCore(CpuContext ctx)
|
||||
@@ -2931,12 +2956,9 @@ public static class KernelMemoryCompatExports
|
||||
return WriteSnprintfOutput(ctx, destination, bufferSize, formatBytes);
|
||||
}
|
||||
|
||||
ulong NextGpArg() => vaCursor.NextGpArg();
|
||||
double NextFloatArg() => vaCursor.NextFloatArg();
|
||||
var rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
|
||||
var rendered = FormatString(ctx, format, ref vaCursor);
|
||||
|
||||
var outputBytes = Encoding.UTF8.GetBytes(rendered);
|
||||
return WriteSnprintfOutput(ctx, destination, bufferSize, outputBytes);
|
||||
return WriteSnprintfOutput(ctx, destination, bufferSize, rendered);
|
||||
}
|
||||
|
||||
private static int SwprintfCore(CpuContext ctx)
|
||||
@@ -2977,9 +2999,7 @@ public static class KernelMemoryCompatExports
|
||||
else
|
||||
{
|
||||
TraceWidePrintfVaList(ctx, "vswprintf", format, vaListAddress, vaCursor);
|
||||
ulong NextGpArg() => vaCursor.NextGpArg();
|
||||
double NextFloatArg() => vaCursor.NextFloatArg();
|
||||
rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
|
||||
rendered = FormatString(ctx, format, ref vaCursor);
|
||||
}
|
||||
|
||||
TraceWidePrintf(ctx, "vswprintf", destination, bufferSize, format, rendered);
|
||||
@@ -2994,10 +3014,8 @@ public static class KernelMemoryCompatExports
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryReadUInt32Compat(ctx, vaListAddress + 0, out var gpOffset) ||
|
||||
!TryReadUInt32Compat(ctx, vaListAddress + 4, out var fpOffset) ||
|
||||
!TryReadUInt64Compat(ctx, vaListAddress + 8, out var overflowArgArea) ||
|
||||
!TryReadUInt64Compat(ctx, vaListAddress + 16, out var regSaveArea))
|
||||
Span<byte> vaList = stackalloc byte[24];
|
||||
if (!TryReadCompat(ctx, vaListAddress, vaList))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -3005,10 +3023,10 @@ public static class KernelMemoryCompatExports
|
||||
cursor = new SysVAmd64VaListCursor(
|
||||
ctx,
|
||||
vaListAddress,
|
||||
gpOffset,
|
||||
fpOffset,
|
||||
overflowArgArea,
|
||||
regSaveArea);
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(vaList),
|
||||
BinaryPrimitives.ReadUInt32LittleEndian(vaList[4..]),
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(vaList[8..]),
|
||||
BinaryPrimitives.ReadUInt64LittleEndian(vaList[16..]));
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -3038,6 +3056,21 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int WriteSnprintfOutput(
|
||||
CpuContext ctx,
|
||||
ulong destination,
|
||||
ulong bufferSize,
|
||||
string output)
|
||||
{
|
||||
if (bufferSize == 0 || destination == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)Encoding.UTF8.GetByteCount(output));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
return WriteSnprintfOutput(ctx, destination, bufferSize, Encoding.UTF8.GetBytes(output));
|
||||
}
|
||||
|
||||
private static int WriteSwprintfOutput(
|
||||
CpuContext ctx,
|
||||
ulong destination,
|
||||
@@ -3329,30 +3362,8 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
internal static string FormatStringFromVarArgs(CpuContext ctx, string format, int firstGpArgIndex)
|
||||
{
|
||||
var gpIndex = Math.Max(0, firstGpArgIndex);
|
||||
|
||||
ulong GetGpArg(int index)
|
||||
{
|
||||
return index switch
|
||||
{
|
||||
0 => ctx[CpuRegister.Rdi],
|
||||
1 => ctx[CpuRegister.Rsi],
|
||||
2 => ctx[CpuRegister.Rdx],
|
||||
3 => ctx[CpuRegister.Rcx],
|
||||
4 => ctx[CpuRegister.R8],
|
||||
5 => ctx[CpuRegister.R9],
|
||||
_ => ReadStackArg(ctx, (ulong)(index - 6) * 8)
|
||||
};
|
||||
}
|
||||
|
||||
ulong NextGpArg() => GetGpArg(gpIndex++);
|
||||
double NextFloatArg()
|
||||
{
|
||||
var rawBits = NextGpArg();
|
||||
return BitConverter.Int64BitsToDouble(unchecked((long)rawBits));
|
||||
}
|
||||
|
||||
return FormatString(ctx, format, NextGpArg, NextFloatArg);
|
||||
var argumentSource = new RegisterPrintfArgumentSource(ctx, Math.Max(0, firstGpArgIndex));
|
||||
return FormatString(ctx, format, ref argumentSource);
|
||||
}
|
||||
|
||||
private static string FormatString(CpuContext ctx, string format)
|
||||
@@ -3360,13 +3371,13 @@ public static class KernelMemoryCompatExports
|
||||
return FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 3);
|
||||
}
|
||||
|
||||
private static string FormatString(
|
||||
private static string FormatString<TArgumentSource>(
|
||||
CpuContext ctx,
|
||||
string format,
|
||||
Func<ulong> nextGpArg,
|
||||
Func<double> nextFloatArg)
|
||||
ref TArgumentSource argumentSource)
|
||||
where TArgumentSource : struct, IPrintfArgumentSource
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
var sb = new StringBuilder(format.Length + 32);
|
||||
|
||||
for (var i = 0; i < format.Length; i++)
|
||||
{
|
||||
@@ -3405,7 +3416,7 @@ public static class KernelMemoryCompatExports
|
||||
var width = 0;
|
||||
if (i < format.Length && format[i] == '*')
|
||||
{
|
||||
width = unchecked((int)nextGpArg());
|
||||
width = unchecked((int)argumentSource.NextGpArg());
|
||||
i++;
|
||||
if (width < 0)
|
||||
{
|
||||
@@ -3428,7 +3439,7 @@ public static class KernelMemoryCompatExports
|
||||
i++;
|
||||
if (i < format.Length && format[i] == '*')
|
||||
{
|
||||
precision = unchecked((int)nextGpArg());
|
||||
precision = unchecked((int)argumentSource.NextGpArg());
|
||||
i++;
|
||||
}
|
||||
else if (i < format.Length && char.IsDigit(format[i]))
|
||||
@@ -3482,14 +3493,14 @@ public static class KernelMemoryCompatExports
|
||||
{
|
||||
long value = lengthMod switch
|
||||
{
|
||||
"hh" => unchecked((sbyte)nextGpArg()),
|
||||
"h" => unchecked((short)nextGpArg()),
|
||||
"l" => unchecked((long)nextGpArg()),
|
||||
"ll" => unchecked((long)nextGpArg()),
|
||||
"j" => unchecked((long)nextGpArg()),
|
||||
"z" => unchecked((long)nextGpArg()),
|
||||
"t" => unchecked((long)nextGpArg()),
|
||||
_ => unchecked((int)nextGpArg())
|
||||
"hh" => unchecked((sbyte)argumentSource.NextGpArg()),
|
||||
"h" => unchecked((short)argumentSource.NextGpArg()),
|
||||
"l" => unchecked((long)argumentSource.NextGpArg()),
|
||||
"ll" => unchecked((long)argumentSource.NextGpArg()),
|
||||
"j" => unchecked((long)argumentSource.NextGpArg()),
|
||||
"z" => unchecked((long)argumentSource.NextGpArg()),
|
||||
"t" => unchecked((long)argumentSource.NextGpArg()),
|
||||
_ => unchecked((int)argumentSource.NextGpArg())
|
||||
};
|
||||
|
||||
var formatted = value.ToString();
|
||||
@@ -3506,14 +3517,14 @@ public static class KernelMemoryCompatExports
|
||||
{
|
||||
ulong value = lengthMod switch
|
||||
{
|
||||
"hh" => (byte)nextGpArg(),
|
||||
"h" => (ushort)nextGpArg(),
|
||||
"l" => nextGpArg(),
|
||||
"ll" => nextGpArg(),
|
||||
"j" => nextGpArg(),
|
||||
"z" => nextGpArg(),
|
||||
"t" => nextGpArg(),
|
||||
_ => (uint)nextGpArg()
|
||||
"hh" => (byte)argumentSource.NextGpArg(),
|
||||
"h" => (ushort)argumentSource.NextGpArg(),
|
||||
"l" => argumentSource.NextGpArg(),
|
||||
"ll" => argumentSource.NextGpArg(),
|
||||
"j" => argumentSource.NextGpArg(),
|
||||
"z" => argumentSource.NextGpArg(),
|
||||
"t" => argumentSource.NextGpArg(),
|
||||
_ => (uint)argumentSource.NextGpArg()
|
||||
};
|
||||
|
||||
var formatted = value.ToString();
|
||||
@@ -3526,14 +3537,14 @@ public static class KernelMemoryCompatExports
|
||||
{
|
||||
ulong value = lengthMod switch
|
||||
{
|
||||
"hh" => (byte)nextGpArg(),
|
||||
"h" => (ushort)nextGpArg(),
|
||||
"l" => nextGpArg(),
|
||||
"ll" => nextGpArg(),
|
||||
"j" => nextGpArg(),
|
||||
"z" => nextGpArg(),
|
||||
"t" => nextGpArg(),
|
||||
_ => (uint)nextGpArg()
|
||||
"hh" => (byte)argumentSource.NextGpArg(),
|
||||
"h" => (ushort)argumentSource.NextGpArg(),
|
||||
"l" => argumentSource.NextGpArg(),
|
||||
"ll" => argumentSource.NextGpArg(),
|
||||
"j" => argumentSource.NextGpArg(),
|
||||
"z" => argumentSource.NextGpArg(),
|
||||
"t" => argumentSource.NextGpArg(),
|
||||
_ => (uint)argumentSource.NextGpArg()
|
||||
};
|
||||
|
||||
var formatted = specifier == 'x'
|
||||
@@ -3551,14 +3562,14 @@ public static class KernelMemoryCompatExports
|
||||
{
|
||||
ulong value = lengthMod switch
|
||||
{
|
||||
"hh" => (byte)nextGpArg(),
|
||||
"h" => (ushort)nextGpArg(),
|
||||
"l" => nextGpArg(),
|
||||
"ll" => nextGpArg(),
|
||||
"j" => nextGpArg(),
|
||||
"z" => nextGpArg(),
|
||||
"t" => nextGpArg(),
|
||||
_ => (uint)nextGpArg()
|
||||
"hh" => (byte)argumentSource.NextGpArg(),
|
||||
"h" => (ushort)argumentSource.NextGpArg(),
|
||||
"l" => argumentSource.NextGpArg(),
|
||||
"ll" => argumentSource.NextGpArg(),
|
||||
"j" => argumentSource.NextGpArg(),
|
||||
"z" => argumentSource.NextGpArg(),
|
||||
"t" => argumentSource.NextGpArg(),
|
||||
_ => (uint)argumentSource.NextGpArg()
|
||||
};
|
||||
|
||||
var formatted = Convert.ToString((long)value, 8);
|
||||
@@ -3571,7 +3582,7 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
case 'p':
|
||||
{
|
||||
var value = nextGpArg();
|
||||
var value = argumentSource.NextGpArg();
|
||||
var formatted = value == 0
|
||||
? "(nil)"
|
||||
: $"0x{value:X}";
|
||||
@@ -3581,7 +3592,7 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
case 's':
|
||||
{
|
||||
var strAddr = nextGpArg();
|
||||
var strAddr = argumentSource.NextGpArg();
|
||||
TracePrintfStringArgument(ctx, lengthMod, strAddr);
|
||||
if (strAddr == 0)
|
||||
{
|
||||
@@ -3620,14 +3631,14 @@ public static class KernelMemoryCompatExports
|
||||
string renderedChar;
|
||||
if (lengthMod == "l")
|
||||
{
|
||||
var scalar = unchecked((ushort)nextGpArg());
|
||||
var scalar = unchecked((ushort)argumentSource.NextGpArg());
|
||||
renderedChar = TryConvertWideScalarToString(scalar, out var wideCharText)
|
||||
? wideCharText
|
||||
: "?";
|
||||
}
|
||||
else
|
||||
{
|
||||
renderedChar = ((char)(byte)nextGpArg()).ToString();
|
||||
renderedChar = ((char)(byte)argumentSource.NextGpArg()).ToString();
|
||||
}
|
||||
|
||||
sb.Append(PadString(renderedChar, width, leftAlign, false));
|
||||
@@ -3641,7 +3652,7 @@ public static class KernelMemoryCompatExports
|
||||
case 'g':
|
||||
case 'G':
|
||||
{
|
||||
var value = nextFloatArg();
|
||||
var value = argumentSource.NextFloatArg();
|
||||
|
||||
var formatStr = precision >= 0
|
||||
? $"{{0:{specifier}{precision}}}"
|
||||
@@ -3659,7 +3670,7 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
case 'n':
|
||||
{
|
||||
var addr = nextGpArg();
|
||||
var addr = argumentSource.NextGpArg();
|
||||
if (addr != 0)
|
||||
{
|
||||
_ = TryWriteInt32(ctx, addr, sb.Length);
|
||||
@@ -3699,7 +3710,46 @@ public static class KernelMemoryCompatExports
|
||||
return leftAlign ? str + padding : padding + str;
|
||||
}
|
||||
|
||||
private struct SysVAmd64VaListCursor
|
||||
private interface IPrintfArgumentSource
|
||||
{
|
||||
ulong NextGpArg();
|
||||
|
||||
double NextFloatArg();
|
||||
}
|
||||
|
||||
private struct RegisterPrintfArgumentSource : IPrintfArgumentSource
|
||||
{
|
||||
private readonly CpuContext _ctx;
|
||||
private int _gpIndex;
|
||||
|
||||
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
|
||||
{
|
||||
_ctx = ctx;
|
||||
_gpIndex = gpIndex;
|
||||
}
|
||||
|
||||
public ulong NextGpArg()
|
||||
{
|
||||
var index = _gpIndex++;
|
||||
return index switch
|
||||
{
|
||||
0 => _ctx[CpuRegister.Rdi],
|
||||
1 => _ctx[CpuRegister.Rsi],
|
||||
2 => _ctx[CpuRegister.Rdx],
|
||||
3 => _ctx[CpuRegister.Rcx],
|
||||
4 => _ctx[CpuRegister.R8],
|
||||
5 => _ctx[CpuRegister.R9],
|
||||
_ => ReadStackArg(_ctx, (ulong)(index - 6) * 8)
|
||||
};
|
||||
}
|
||||
|
||||
public double NextFloatArg()
|
||||
{
|
||||
return BitConverter.Int64BitsToDouble(unchecked((long)NextGpArg()));
|
||||
}
|
||||
}
|
||||
|
||||
private struct SysVAmd64VaListCursor : IPrintfArgumentSource
|
||||
{
|
||||
private const uint GpSaveAreaLimit = 48;
|
||||
private const uint FpSaveAreaLimit = 176;
|
||||
@@ -4011,6 +4061,11 @@ public static class KernelMemoryCompatExports
|
||||
return guestPath;
|
||||
}
|
||||
|
||||
if (TryResolveRegisteredGuestMount(guestPath, out var mountedPath))
|
||||
{
|
||||
return mountedPath;
|
||||
}
|
||||
|
||||
if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var relative = NormalizeMountRelativePath(guestPath["/devlog/app/".Length..]);
|
||||
@@ -4109,6 +4164,51 @@ public static class KernelMemoryCompatExports
|
||||
return guestPath;
|
||||
}
|
||||
|
||||
private static bool TryResolveRegisteredGuestMount(string guestPath, out string hostPath)
|
||||
{
|
||||
hostPath = string.Empty;
|
||||
var normalizedGuestPath = NormalizeGuestStatCachePath(guestPath);
|
||||
if (normalizedGuestPath is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string? matchedMountPoint = null;
|
||||
string? matchedHostRoot = null;
|
||||
lock (_guestMountGate)
|
||||
{
|
||||
foreach (var (mountPoint, hostRoot) in _guestMounts)
|
||||
{
|
||||
if ((string.Equals(normalizedGuestPath, mountPoint, StringComparison.OrdinalIgnoreCase) ||
|
||||
normalizedGuestPath.StartsWith(mountPoint + "/", StringComparison.OrdinalIgnoreCase)) &&
|
||||
(matchedMountPoint is null || mountPoint.Length > matchedMountPoint.Length))
|
||||
{
|
||||
matchedMountPoint = mountPoint;
|
||||
matchedHostRoot = hostRoot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedMountPoint is null || matchedHostRoot is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var relativePath = normalizedGuestPath[matchedMountPoint.Length..].TrimStart('/');
|
||||
var candidate = Path.GetFullPath(Path.Combine(
|
||||
matchedHostRoot,
|
||||
NormalizeMountRelativePath(relativePath)));
|
||||
var rootWithSeparator = Path.TrimEndingDirectorySeparator(matchedHostRoot) + Path.DirectorySeparatorChar;
|
||||
if (!string.Equals(candidate, matchedHostRoot, StringComparison.OrdinalIgnoreCase) &&
|
||||
!candidate.StartsWith(rootWithSeparator, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
hostPath = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? ResolveApp0Root()
|
||||
{
|
||||
var cached = Volatile.Read(ref _cachedApp0Root);
|
||||
@@ -4297,9 +4397,32 @@ public static class KernelMemoryCompatExports
|
||||
}
|
||||
|
||||
const int maxChunkSize = 4096;
|
||||
const int inlineChunkSize = 256;
|
||||
Span<byte> inlineChunk = stackalloc byte[inlineChunkSize];
|
||||
var firstPageRemaining = maxChunkSize - (int)(address & (maxChunkSize - 1));
|
||||
var firstReadLength = Math.Min(limit, Math.Min(inlineChunkSize, firstPageRemaining));
|
||||
var firstSpan = inlineChunk[..firstReadLength];
|
||||
ulong offset = 0;
|
||||
if (TryReadCompat(ctx, address, firstSpan))
|
||||
{
|
||||
var nulIndex = firstSpan.IndexOf((byte)0);
|
||||
if (nulIndex >= 0)
|
||||
{
|
||||
bytes = firstSpan[..nulIndex].ToArray();
|
||||
return true;
|
||||
}
|
||||
|
||||
offset = unchecked((ulong)firstReadLength);
|
||||
}
|
||||
|
||||
var chunk = GC.AllocateUninitializedArray<byte>(Math.Min(maxChunkSize, limit));
|
||||
var writer = new ArrayBufferWriter<byte>(Math.Min(limit, 256));
|
||||
ulong offset = 0;
|
||||
if (offset != 0)
|
||||
{
|
||||
firstSpan.CopyTo(writer.GetSpan(firstReadLength));
|
||||
writer.Advance(firstReadLength);
|
||||
}
|
||||
|
||||
while (offset < (ulong)limit)
|
||||
{
|
||||
var current = address + offset;
|
||||
|
||||
@@ -11,7 +11,6 @@ namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelPthreadCompatExports
|
||||
{
|
||||
private const int MutexTypeDefault = 0;
|
||||
private const int MutexTypeErrorCheck = 1;
|
||||
private const int MutexTypeRecursive = 2;
|
||||
private const int MutexTypeNormal = 3;
|
||||
@@ -44,7 +43,7 @@ public static class KernelPthreadCompatExports
|
||||
public SemaphoreSlim Semaphore { get; } = new(1, 1);
|
||||
public ulong OwnerThreadId { get; set; }
|
||||
public int RecursionCount { get; set; }
|
||||
public int Type { get; set; } = MutexTypeDefault;
|
||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||
public int Protocol { get; set; }
|
||||
}
|
||||
|
||||
@@ -541,7 +540,7 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (state.Type is MutexTypeDefault or MutexTypeNormal or MutexTypeAdaptiveNp)
|
||||
if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp)
|
||||
{
|
||||
if (tryOnly)
|
||||
{
|
||||
@@ -676,7 +675,7 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var initialState = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -735,7 +734,7 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
|
||||
{
|
||||
state = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||
state = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||
}
|
||||
|
||||
updatedState = state with { Type = NormalizeMutexType(type) };
|
||||
@@ -764,7 +763,7 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
|
||||
{
|
||||
state = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||
state = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||
}
|
||||
|
||||
updatedState = state with { Protocol = protocol };
|
||||
@@ -847,7 +846,7 @@ public static class KernelPthreadCompatExports
|
||||
return false;
|
||||
}
|
||||
|
||||
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeDefault, out resolvedAddress, out state);
|
||||
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeErrorCheck, out resolvedAddress, out state);
|
||||
}
|
||||
|
||||
private static ulong ResolveMutexAttrHandle(CpuContext ctx, ulong attrAddress)
|
||||
@@ -891,7 +890,7 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
return _mutexAttrStates.TryGetValue(resolvedAddress, out var state)
|
||||
? state
|
||||
: new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||
: new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1293,12 +1292,12 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
0 => MutexTypeDefault,
|
||||
0 => MutexTypeErrorCheck,
|
||||
1 => MutexTypeErrorCheck,
|
||||
2 => MutexTypeRecursive,
|
||||
3 => MutexTypeNormal,
|
||||
4 => MutexTypeAdaptiveNp,
|
||||
_ => MutexTypeDefault,
|
||||
_ => MutexTypeErrorCheck,
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
@@ -13,13 +13,13 @@ namespace SharpEmu.Libs.Kernel;
|
||||
public static class KernelPthreadExtendedCompatExports
|
||||
{
|
||||
private const int DefaultThreadPriority = 700;
|
||||
private const ulong DefaultThreadAffinityMask = ulong.MaxValue;
|
||||
private const ulong DefaultThreadAffinityMask = 0x7FUL;
|
||||
private const int DefaultDetachState = 0;
|
||||
private const ulong DefaultGuardSize = 0x1000UL;
|
||||
private const ulong DefaultStackSize = 0x1_00000UL;
|
||||
private const int DefaultInheritSched = 0;
|
||||
private const int DefaultSchedPolicy = 0;
|
||||
private const int DefaultSchedPriority = 0;
|
||||
private const int DefaultInheritSched = 4;
|
||||
private const int DefaultSchedPolicy = 1;
|
||||
private const int DefaultSchedPriority = DefaultThreadPriority;
|
||||
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
|
||||
private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_0000_0000;
|
||||
|
||||
@@ -34,6 +34,48 @@ public static class KernelPthreadExtendedCompatExports
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
||||
|
||||
internal static void GetThreadStartScheduling(
|
||||
CpuContext ctx,
|
||||
ulong attrAddress,
|
||||
out int priority,
|
||||
out ulong affinityMask)
|
||||
{
|
||||
if (attrAddress == 0)
|
||||
{
|
||||
priority = DefaultThreadPriority;
|
||||
affinityMask = DefaultThreadAffinityMask;
|
||||
return;
|
||||
}
|
||||
|
||||
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
|
||||
lock (_stateGate)
|
||||
{
|
||||
var attributes = GetOrCreateAttrStateLocked(resolvedAddress);
|
||||
priority = attributes.SchedPriority;
|
||||
affinityMask = attributes.AffinityMask;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void RegisterThreadStart(
|
||||
ulong thread,
|
||||
string name,
|
||||
int priority,
|
||||
ulong affinityMask)
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
var state = GetOrCreateThreadStateLocked(thread);
|
||||
state.Name = name;
|
||||
state.Priority = priority;
|
||||
state.AffinityMask = affinityMask;
|
||||
state.Attributes = state.Attributes with
|
||||
{
|
||||
SchedPriority = priority,
|
||||
AffinityMask = affinityMask,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private sealed class ThreadState
|
||||
{
|
||||
public string Name { get; set; } = string.Empty;
|
||||
|
||||
@@ -0,0 +1,76 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Np;
|
||||
|
||||
public static class NpEntitlementAccessExports
|
||||
{
|
||||
private const int BootParamClearSize = 0x20;
|
||||
private const int EmptyAddcontInfoListSize = 0x10;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jO8DM8oyego",
|
||||
ExportName = "sceNpEntitlementAccessInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpEntitlementAccess")]
|
||||
public static int NpEntitlementAccessInitialize(CpuContext ctx)
|
||||
{
|
||||
var initParam = ctx[CpuRegister.Rdi];
|
||||
var bootParam = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (bootParam != 0)
|
||||
{
|
||||
Span<byte> clear = stackalloc byte[BootParamClearSize];
|
||||
clear.Clear();
|
||||
if (!ctx.Memory.TryWrite(bootParam, clear))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TFyU+KFBv54",
|
||||
ExportName = "sceNpEntitlementAccessGetAddcontEntitlementInfoList",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpEntitlementAccess")]
|
||||
public static int NpEntitlementAccessGetAddcontEntitlementInfoList(CpuContext ctx)
|
||||
{
|
||||
var listAddress = ctx[CpuRegister.Rsi];
|
||||
if (listAddress != 0)
|
||||
{
|
||||
Span<byte> emptyList = stackalloc byte[EmptyAddcontInfoListSize];
|
||||
emptyList.Clear();
|
||||
if (!ctx.Memory.TryWrite(listAddress, emptyList))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
TraceNpEntitlementAccess(
|
||||
$"get_addcont_info_list service=0x{ctx[CpuRegister.Rdi]:X16} list=0x{listAddress:X16} " +
|
||||
$"max={ctx[CpuRegister.Rdx]} flags=0x{ctx[CpuRegister.Rcx]:X16} -> empty");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
private static void TraceNpEntitlementAccess(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] np.entitlement.{message}");
|
||||
}
|
||||
}
|
||||
@@ -29,6 +29,7 @@ public static class PlayGoExports
|
||||
private const int PlayGoInstallSpeedTrickle = 1;
|
||||
private const int PlayGoInstallSpeedFull = 2;
|
||||
private const uint MaxPlayGoQueryEntries = 0x4000;
|
||||
private const uint PlayGoAllEntriesSentinel = uint.MaxValue;
|
||||
|
||||
private static readonly Regex ChunkIdPattern = new(
|
||||
@"<chunk\s+[^>]*\bid\s*=\s*""(?<id>\d+)""",
|
||||
@@ -45,6 +46,7 @@ public static class PlayGoExports
|
||||
private static int _installSpeed = PlayGoInstallSpeedTrickle;
|
||||
private static ulong _languageMask = ulong.MaxValue;
|
||||
private static int _unknownChunkDiagnostics;
|
||||
private static int _locusTraceDiagnostics;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ts6GlZOKRrE",
|
||||
@@ -94,6 +96,7 @@ public static class PlayGoExports
|
||||
_languageMask = ulong.MaxValue;
|
||||
_opened = false;
|
||||
_initialized = true;
|
||||
TracePlayGo($"initialize chunks={_metadata.ChunkIds.Length} available={_metadata.Available}");
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -131,6 +134,7 @@ public static class PlayGoExports
|
||||
}
|
||||
|
||||
_opened = true;
|
||||
TracePlayGo($"open handle={PlayGoHandle} chunks={_metadata.ChunkIds.Length}");
|
||||
}
|
||||
|
||||
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
|
||||
@@ -229,6 +233,7 @@ public static class PlayGoExports
|
||||
var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length;
|
||||
if (outChunkIdList == 0)
|
||||
{
|
||||
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
|
||||
return TryWriteUInt32(ctx, outEntries, availableEntries)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -237,6 +242,7 @@ public static class PlayGoExports
|
||||
var entriesToWrite = Math.Min(numberOfEntries, availableEntries);
|
||||
if (entriesToWrite > MaxPlayGoQueryEntries)
|
||||
{
|
||||
TracePlayGo($"get_chunk_id bad_size requested={numberOfEntries} available={availableEntries}");
|
||||
return OrbisPlayGoErrorBadSize;
|
||||
}
|
||||
|
||||
@@ -249,6 +255,7 @@ public static class PlayGoExports
|
||||
}
|
||||
}
|
||||
|
||||
TracePlayGo($"get_chunk_id write requested={numberOfEntries} wrote={entriesToWrite} available={availableEntries}");
|
||||
return TryWriteUInt32(ctx, outEntries, entriesToWrite)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -279,6 +286,7 @@ public static class PlayGoExports
|
||||
|
||||
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
|
||||
{
|
||||
TracePlayGo($"get_eta bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outEta:X16}");
|
||||
return OrbisPlayGoErrorBadSize;
|
||||
}
|
||||
|
||||
@@ -376,8 +384,15 @@ public static class PlayGoExports
|
||||
return OrbisPlayGoErrorBadPointer;
|
||||
}
|
||||
|
||||
if (numberOfEntries == PlayGoAllEntriesSentinel)
|
||||
{
|
||||
TracePlayGo($"get_locus sentinel entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
|
||||
{
|
||||
TracePlayGo($"get_locus bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
|
||||
return OrbisPlayGoErrorBadSize;
|
||||
}
|
||||
|
||||
@@ -408,6 +423,7 @@ public static class PlayGoExports
|
||||
loci[i] = PlayGoLocusLocalFast;
|
||||
}
|
||||
|
||||
TracePlayGoLocus(numberOfEntries, chunkIds, outLoci);
|
||||
return ctx.Memory.TryWrite(outLoci, loci)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -438,6 +454,7 @@ public static class PlayGoExports
|
||||
|
||||
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
|
||||
{
|
||||
TracePlayGo($"get_progress bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outProgress:X16}");
|
||||
return OrbisPlayGoErrorBadSize;
|
||||
}
|
||||
|
||||
@@ -447,6 +464,7 @@ public static class PlayGoExports
|
||||
return chunkError;
|
||||
}
|
||||
|
||||
TracePlayGo($"get_progress entries={numberOfEntries}");
|
||||
Span<byte> progress = stackalloc byte[sizeof(ulong) * 2];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(progress, 0);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(progress[sizeof(ulong)..], 0);
|
||||
@@ -480,9 +498,11 @@ public static class PlayGoExports
|
||||
|
||||
if (numberOfEntries == 0)
|
||||
{
|
||||
TracePlayGo("get_todo bad_size entries=0");
|
||||
return OrbisPlayGoErrorBadSize;
|
||||
}
|
||||
|
||||
TracePlayGo($"get_todo requested={numberOfEntries} wrote=0");
|
||||
return TryWriteUInt32(ctx, outEntries, 0)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -749,6 +769,29 @@ public static class PlayGoExports
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static void TracePlayGo(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] playgo.{message}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void TracePlayGoLocus(uint entries, ulong chunkIds, ulong outLoci)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var count = Interlocked.Increment(ref _locusTraceDiagnostics);
|
||||
if (entries != 1 || count <= 32 || count % 1000 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] playgo.get_locus entries={entries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
private sealed record PlayGoMetadata(bool Available, ushort[] ChunkIds)
|
||||
{
|
||||
public static readonly PlayGoMetadata Empty = new(false, Array.Empty<ushort>());
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.SaveData;
|
||||
|
||||
public static class SaveDataDialogExports
|
||||
{
|
||||
private const int StatusNone = 0;
|
||||
private const int StatusInitialized = 1;
|
||||
private const int StatusRunning = 2;
|
||||
private const int StatusFinished = 3;
|
||||
|
||||
private const int ErrorOk = 0;
|
||||
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
|
||||
private const int ErrorAlreadyInitialized = unchecked((int)0x80B80004);
|
||||
private const int ErrorNotFinished = unchecked((int)0x80B80005);
|
||||
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
|
||||
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
||||
|
||||
private const int ResultSize = 0x48;
|
||||
private static int _status;
|
||||
private static int _lastMode;
|
||||
private static ulong _lastUserData;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "s9e3+YpRnzw",
|
||||
ExportName = "sceSaveDataDialogInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
|
||||
{
|
||||
return SetReturn(ctx, ErrorAlreadyInitialized);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4tPhsP6FpDI",
|
||||
ExportName = "sceSaveDataDialogOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogOpen(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, ErrorArgNull);
|
||||
}
|
||||
|
||||
if (_status is not (StatusInitialized or StatusFinished))
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotInitialized);
|
||||
}
|
||||
|
||||
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
|
||||
_lastUserData = TryReadUInt64(ctx, paramAddress + 0xC8, out var userData) ? userData : 0;
|
||||
|
||||
// There is no host save dialog yet. Complete immediately with OK so
|
||||
// guest polling sees a finished dialog instead of spinning forever.
|
||||
Interlocked.Exchange(ref _status, StatusFinished);
|
||||
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished");
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ERKzksauAJA",
|
||||
ExportName = "sceSaveDataDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KK3Bdg1RWK0",
|
||||
ExportName = "sceSaveDataDialogUpdateStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "en7gNVnh878",
|
||||
ExportName = "sceSaveDataDialogIsReadyToDisplay",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => SetReturn(ctx, 1);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yEiJ-qqr6Cg",
|
||||
ExportName = "sceSaveDataDialogGetResult",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogGetResult(CpuContext ctx)
|
||||
{
|
||||
var resultAddress = ctx[CpuRegister.Rdi];
|
||||
if (resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) != StatusFinished)
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotFinished);
|
||||
}
|
||||
|
||||
Span<byte> result = stackalloc byte[ResultSize];
|
||||
result.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x00..], _lastMode);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], 0);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(result[0x20..], _lastUserData);
|
||||
|
||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fH46Lag88XY",
|
||||
ExportName = "sceSaveDataDialogClose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogClose(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotRunning);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "YuH2FA7azqQ",
|
||||
ExportName = "sceSaveDataDialogTerminate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogTerminate(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotInitialized);
|
||||
}
|
||||
|
||||
_lastMode = 0;
|
||||
_lastUserData = 0;
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "V-uEeFKARJU",
|
||||
ExportName = "sceSaveDataDialogProgressBarInc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => SetReturn(ctx, ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hay1CfTmLyA",
|
||||
ExportName = "sceSaveDataDialogProgressBarSetValue",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => SetReturn(ctx, ErrorOk);
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
value = 0;
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
value = 0;
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceSaveDataDialog(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] save_data_dialog.{message}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.Libs.SaveData;
|
||||
|
||||
public static class SaveDataExports
|
||||
{
|
||||
private const int OrbisSaveDataErrorParameter = unchecked((int)0x809F0000);
|
||||
private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B);
|
||||
private const int SaveDataTitleIdSize = 10;
|
||||
private const int SaveDataDirNameSize = 32;
|
||||
private const int SaveDataParamSize = 0x530;
|
||||
private const int SaveDataSearchInfoSize = 0x30;
|
||||
private const ulong ResultHitNumOffset = 0x00;
|
||||
private const ulong ResultDirNamesOffset = 0x08;
|
||||
private const ulong ResultDirNamesNumOffset = 0x10;
|
||||
private const ulong ResultSetNumOffset = 0x14;
|
||||
private const ulong ResultParamsOffset = 0x18;
|
||||
private const ulong ResultInfosOffset = 0x20;
|
||||
private const uint SortKeyFreeBlocks = 5;
|
||||
private const uint SortOrderDescent = 1;
|
||||
private static readonly object _stateGate = new();
|
||||
private static string? _titleId;
|
||||
|
||||
public static void ConfigureApplicationInfo(string? titleId)
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TywrFKCoLGY",
|
||||
ExportName = "sceSaveDataInitialize3",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataInitialize3(CpuContext ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ResolveSaveDataRoot());
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "dyIhnXq-0SM",
|
||||
ExportName = "sceSaveDataDirNameSearch",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataDirNameSearch(CpuContext ctx)
|
||||
{
|
||||
var condAddress = ctx[CpuRegister.Rdi];
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
if (condAddress == 0 || resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
|
||||
!TryReadSearchResult(ctx, resultAddress, out var result))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string titleId;
|
||||
if (cond.TitleIdAddress == 0)
|
||||
{
|
||||
titleId = ResolveConfiguredTitleId();
|
||||
}
|
||||
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
|
||||
var entries = Directory.Exists(root)
|
||||
? EnumerateSaveDirectories(root, cond.Pattern)
|
||||
: [];
|
||||
|
||||
entries = SortEntries(entries, cond.SortKey, cond.SortOrder);
|
||||
var setNum = result.DirNamesNum == 0
|
||||
? 0
|
||||
: Math.Min(result.DirNamesNum, entries.Count);
|
||||
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
||||
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (setNum == 0)
|
||||
{
|
||||
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
if (result.DirNamesAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
for (var i = 0; i < setNum; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
if (!TryWriteFixedAscii(
|
||||
ctx,
|
||||
result.DirNamesAddress + ((ulong)i * SaveDataDirNameSize),
|
||||
SaveDataDirNameSize,
|
||||
entry.Name) ||
|
||||
(result.ParamsAddress != 0 &&
|
||||
!TryWriteParam(ctx, result.ParamsAddress + ((ulong)i * SaveDataParamSize), entry)) ||
|
||||
(result.InfosAddress != 0 &&
|
||||
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
||||
{
|
||||
cond = default;
|
||||
if (!TryReadInt32(ctx, address, out var userId) ||
|
||||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
||||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
||||
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
|
||||
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string pattern;
|
||||
if (dirNameAddress == 0)
|
||||
{
|
||||
pattern = string.Empty;
|
||||
}
|
||||
else if (!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out pattern))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
cond = new SearchCond(userId, titleIdAddress, pattern, sortKey, sortOrder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadSearchResult(CpuContext ctx, ulong address, out SearchResult result)
|
||||
{
|
||||
result = default;
|
||||
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
|
||||
!TryReadUInt32(ctx, address + ResultDirNamesNumOffset, out var dirNamesNum) ||
|
||||
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
|
||||
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new SearchResult(dirNamesAddress, dirNamesNum, paramsAddress, infosAddress);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<SaveEntry> EnumerateSaveDirectories(string root, string pattern)
|
||||
{
|
||||
var entries = new List<SaveEntry>();
|
||||
foreach (var directory in Directory.EnumerateDirectories(root))
|
||||
{
|
||||
var name = Path.GetFileName(directory);
|
||||
if (string.IsNullOrWhiteSpace(name) ||
|
||||
name.StartsWith("sce_", StringComparison.OrdinalIgnoreCase) ||
|
||||
(!string.IsNullOrEmpty(pattern) && !MatchPattern(name, pattern)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var info = new DirectoryInfo(directory);
|
||||
entries.Add(new SaveEntry(name, directory, info.LastWriteTimeUtc));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static List<SaveEntry> SortEntries(List<SaveEntry> entries, uint sortKey, uint sortOrder)
|
||||
{
|
||||
IOrderedEnumerable<SaveEntry> sorted = sortKey switch
|
||||
{
|
||||
3 => entries.OrderBy(entry => entry.LastWriteUtc),
|
||||
_ => entries.OrderBy(entry => entry.Name, StringComparer.OrdinalIgnoreCase),
|
||||
};
|
||||
|
||||
var list = sorted.ToList();
|
||||
if (sortOrder == SortOrderDescent)
|
||||
{
|
||||
list.Reverse();
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static bool TryWriteParam(CpuContext ctx, ulong address, SaveEntry entry)
|
||||
{
|
||||
var param = new byte[SaveDataParamSize];
|
||||
WriteAscii(param.AsSpan(0x00, 128), "Saved Data");
|
||||
WriteAscii(param.AsSpan(0x100, 1024), entry.Name);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(
|
||||
param.AsSpan(0x508, sizeof(long)),
|
||||
new DateTimeOffset(entry.LastWriteUtc).ToUnixTimeSeconds());
|
||||
return ctx.Memory.TryWrite(address, param);
|
||||
}
|
||||
|
||||
private static bool TryWriteSearchInfo(CpuContext ctx, ulong address, SaveEntry entry)
|
||||
{
|
||||
var size = GetDirectorySize(entry.Path);
|
||||
var usedBlocks = checked((ulong)((size + 32767) / 32768));
|
||||
var blocks = Math.Max(96UL, usedBlocks);
|
||||
Span<byte> info = stackalloc byte[SaveDataSearchInfoSize];
|
||||
info.Clear();
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(info[0x00..], blocks);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(info[0x08..], blocks - usedBlocks);
|
||||
return ctx.Memory.TryWrite(address, info);
|
||||
}
|
||||
|
||||
private static long GetDirectorySize(string root)
|
||||
{
|
||||
long total = 0;
|
||||
foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
total += new FileInfo(file).Length;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static bool MatchPattern(string value, string pattern) =>
|
||||
MatchPattern(value.AsSpan(), pattern.AsSpan());
|
||||
|
||||
private static bool MatchPattern(ReadOnlySpan<char> value, ReadOnlySpan<char> pattern)
|
||||
{
|
||||
if (pattern.IsEmpty)
|
||||
{
|
||||
return value.IsEmpty;
|
||||
}
|
||||
|
||||
if (pattern[0] == '%')
|
||||
{
|
||||
for (var i = 0; i <= value.Length; i++)
|
||||
{
|
||||
if (MatchPattern(value[i..], pattern[1..]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pattern[0] == '_' ||
|
||||
char.ToUpperInvariant(pattern[0]) == char.ToUpperInvariant(value[0]))
|
||||
{
|
||||
return MatchPattern(value[1..], pattern[1..]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ResolveTitleSaveRoot(int userId, string titleId) =>
|
||||
Path.Combine(ResolveSaveDataRoot(), userId.ToString(), SanitizePathSegment(titleId));
|
||||
|
||||
private static string ResolveSaveDataRoot()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
||||
var root = string.IsNullOrWhiteSpace(configured)
|
||||
? Path.Combine(Environment.CurrentDirectory, "user", "savedata")
|
||||
: configured;
|
||||
return Path.GetFullPath(root);
|
||||
}
|
||||
|
||||
private static string ResolveConfiguredTitleId()
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_titleId))
|
||||
{
|
||||
return _titleId;
|
||||
}
|
||||
}
|
||||
|
||||
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||
var app0Name = string.IsNullOrWhiteSpace(app0Root)
|
||||
? null
|
||||
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
|
||||
if (!string.IsNullOrWhiteSpace(app0Name))
|
||||
{
|
||||
var candidate = app0Name.Split('-', StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
if (!string.IsNullOrWhiteSpace(candidate))
|
||||
{
|
||||
return SanitizePathSegment(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return "default";
|
||||
}
|
||||
|
||||
private static string SanitizePathSegment(string value)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
var sanitized = new string(value.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray());
|
||||
return string.IsNullOrWhiteSpace(sanitized) ? "default" : sanitized;
|
||||
}
|
||||
|
||||
private static bool TryReadFixedAscii(CpuContext ctx, ulong address, int length, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
Span<byte> buffer = stackalloc byte[length];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stringLength = buffer.IndexOf((byte)0);
|
||||
if (stringLength < 0)
|
||||
{
|
||||
stringLength = buffer.Length;
|
||||
}
|
||||
|
||||
value = Encoding.ASCII.GetString(buffer[..stringLength]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteFixedAscii(CpuContext ctx, ulong address, int length, string value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[length];
|
||||
buffer.Clear();
|
||||
WriteAscii(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static void WriteAscii(Span<byte> destination, string value)
|
||||
{
|
||||
var count = Math.Min(value.Length, Math.Max(0, destination.Length - 1));
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var ch = value[i];
|
||||
destination[i] = ch <= 0x7F ? (byte)ch : (byte)'?';
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceSaveData(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] savedata.{message}");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct SearchCond(
|
||||
int UserId,
|
||||
ulong TitleIdAddress,
|
||||
string Pattern,
|
||||
uint SortKey,
|
||||
uint SortOrder);
|
||||
|
||||
private readonly record struct SearchResult(
|
||||
ulong DirNamesAddress,
|
||||
uint DirNamesNum,
|
||||
ulong ParamsAddress,
|
||||
ulong InfosAddress);
|
||||
|
||||
private readonly record struct SaveEntry(string Name, string Path, DateTime LastWriteUtc);
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.SystemService;
|
||||
@@ -85,6 +86,17 @@ public static class SystemServiceExports
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Vo5V8KAwCmk",
|
||||
ExportName = "sceSystemServiceHideSplashScreen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceHideSplashScreen(CpuContext ctx)
|
||||
{
|
||||
VulkanVideoPresenter.HideSplashScreen();
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
|
||||
@@ -363,6 +363,23 @@ public static class VideoOutExports
|
||||
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "zgXifHT9ErY",
|
||||
ExportName = "sceVideoOutIsFlipPending",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutIsFlipPending(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!TryGetPort(handle, out _))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "U2JJtSqNKZI",
|
||||
ExportName = "sceVideoOutGetEventId",
|
||||
|
||||
@@ -26,6 +26,7 @@ internal static unsafe class VulkanVideoPresenter
|
||||
private static uint _windowWidth;
|
||||
private static uint _windowHeight;
|
||||
private static bool _closed;
|
||||
private static bool _splashHidden;
|
||||
|
||||
public static void EnsureStarted(uint width, uint height)
|
||||
{
|
||||
@@ -55,7 +56,15 @@ internal static unsafe class VulkanVideoPresenter
|
||||
|
||||
_windowWidth = width;
|
||||
_windowHeight = height;
|
||||
_latestPresentation ??= hasSplash
|
||||
_latestPresentation ??= _splashHidden
|
||||
? new Presentation(
|
||||
CreateBlackFrame(width, height),
|
||||
width,
|
||||
height,
|
||||
1,
|
||||
GuestDrawKind.None,
|
||||
IsSplash: false)
|
||||
: hasSplash
|
||||
? new Presentation(
|
||||
splashPixels,
|
||||
splashWidth,
|
||||
@@ -79,6 +88,28 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
public static void HideSplashScreen()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_splashHidden = true;
|
||||
if (_closed || _latestPresentation is not { IsSplash: true } latest)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var sequence = latest.Sequence + 1;
|
||||
_latestPresentation = new Presentation(
|
||||
CreateBlackFrame(latest.Width, latest.Height),
|
||||
latest.Width,
|
||||
latest.Height,
|
||||
sequence,
|
||||
GuestDrawKind.None,
|
||||
IsSplash: false);
|
||||
Console.Error.WriteLine("[LOADER][INFO] Vulkan VideoOut hid splash");
|
||||
}
|
||||
}
|
||||
|
||||
public static void Submit(byte[] bgraFrame, uint width, uint height)
|
||||
{
|
||||
if (bgraFrame.Length != checked((int)(width * height * 4)))
|
||||
@@ -159,6 +190,24 @@ internal static unsafe class VulkanVideoPresenter
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] CreateBlackFrame(uint width, uint height)
|
||||
{
|
||||
if (width == 0 || height == 0 || width > 8192 || height > 8192)
|
||||
{
|
||||
width = 1;
|
||||
height = 1;
|
||||
}
|
||||
|
||||
var pixels = GC.AllocateUninitializedArray<byte>(checked((int)(width * height * 4)));
|
||||
pixels.AsSpan().Clear();
|
||||
for (var offset = 3; offset < pixels.Length; offset += 4)
|
||||
{
|
||||
pixels[offset] = 0xFF;
|
||||
}
|
||||
|
||||
return pixels;
|
||||
}
|
||||
|
||||
private static void Run()
|
||||
{
|
||||
uint width;
|
||||
@@ -259,6 +308,8 @@ internal static unsafe class VulkanVideoPresenter
|
||||
options.Title = VideoOutExports.GetWindowTitle();
|
||||
options.WindowBorder = WindowBorder.Fixed;
|
||||
options.VSync = true;
|
||||
options.FramesPerSecond = 60;
|
||||
options.UpdatesPerSecond = 60;
|
||||
_window = Window.Create(options);
|
||||
_window.Load += Initialize;
|
||||
_window.Render += Render;
|
||||
|
||||
Reference in New Issue
Block a user