Compare commits

...

9 Commits

Author SHA1 Message Date
ParantezTech 873f473b65 [video] hide splash 2026-06-29 13:32:17 +03:00
ParantezTech 9a1a3789ef [video] cap presenter ticks 2026-06-29 13:32:03 +03:00
ParantezTech 0f3d4032cb [core] add leaf imports 2026-06-29 13:30:50 +03:00
ParantezTech 0c4a757695 [core] log leaf import args 2026-06-29 13:30:32 +03:00
ParantezTech 5fcc8eaa37 [saveData] add dir search 2026-06-29 13:30:08 +03:00
ParantezTech 21105e0346 [ampr] batch buffer IO 2026-06-29 13:28:37 +03:00
ParantezTech b424df5a64 [kernel] speed up printf 2026-06-29 13:28:30 +03:00
ParantezTech 7f85971e39 [kernel] carry pthread scheduling 2026-06-29 13:27:59 +03:00
ParantezTech e28259a99d [core] speed up guest memory 2026-06-29 13:26:04 +03:00
12 changed files with 1111 additions and 199 deletions
@@ -527,7 +527,10 @@ public sealed partial class DirectExecutionBackend
if (dispatchIndex % 100000 == 0) if (dispatchIndex % 100000 == 0)
{ {
Console.Error.WriteLine( 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( var previousImportCallFrame = GuestThreadExecution.EnterImportCallFrame(
@@ -653,7 +656,12 @@ public sealed partial class DirectExecutionBackend
"rqwFKI4PAiM" or "rqwFKI4PAiM" or
"eE4Szl8sil8" or "eE4Szl8sil8" or
"qvMUCyyaCSI" or "qvMUCyyaCSI" or
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
"TywrFKCoLGY" or // sceSaveDataInitialize3
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
"27bAgiJmOh0" or // pthread_cond_timedwait "27bAgiJmOh0" or // pthread_cond_timedwait
"iQw3iQPhvUQ" or // sceNetCtlCheckCallback
"Q2V+iqvjgC0" or // vsnprintf
"j4ViWNHEgww" or // strlen "j4ViWNHEgww" or // strlen
"5jNubw4vlAA" or // strnlen "5jNubw4vlAA" or // strnlen
"LHMrG7e8G78" or // wcslen "LHMrG7e8G78" or // wcslen
@@ -347,6 +347,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
public string Name { get; init; } = string.Empty; public string Name { get; init; } = string.Empty;
public int Priority { get; init; }
public ulong AffinityMask { get; init; }
public CpuContext Context { get; init; } = null!; public CpuContext Context { get; init; } = null!;
public GuestThreadRunState State { get; set; } public GuestThreadRunState State { get; set; }
@@ -388,14 +392,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private Action? _work; private Action? _work;
private volatile bool _stopping; private volatile bool _stopping;
public GuestContinuationRunner(ulong guestThreadHandle) public GuestContinuationRunner(ulong guestThreadHandle, ThreadPriority priority)
{ {
_guestThreadHandle = guestThreadHandle; _guestThreadHandle = guestThreadHandle;
_thread = new Thread(ThreadMain) _thread = new Thread(ThreadMain)
{ {
IsBackground = true, IsBackground = true,
Name = $"GuestContinuation-{guestThreadHandle:X}", Name = $"GuestContinuation-{guestThreadHandle:X}",
Priority = ThreadPriority.BelowNormal, Priority = priority,
}; };
_thread.Start(); _thread.Start();
} }
@@ -2393,7 +2397,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
Interlocked.Increment(ref _readyGuestThreadCount); Interlocked.Increment(ref _readyGuestThreadCount);
} }
Console.Error.WriteLine( 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"); Pump(creatorContext, "pthread_create");
return true; return true;
} }
@@ -2448,7 +2454,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{ {
IsBackground = true, IsBackground = true,
Name = $"SharpEmu-{thread.Name}", Name = $"SharpEmu-{thread.Name}",
Priority = ThreadPriority.BelowNormal, Priority = MapGuestThreadPriority(thread.Priority),
}; };
lock (_guestThreadGate) lock (_guestThreadGate)
{ {
@@ -2851,7 +2857,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{ {
if (_guestThreads.TryGetValue(currentGuestThreadHandle, out var guestThread)) if (_guestThreads.TryGetValue(currentGuestThreadHandle, out var guestThread))
{ {
runner = guestThread.ContinuationRunner ??= new GuestContinuationRunner(currentGuestThreadHandle); runner = guestThread.ContinuationRunner ??= new GuestContinuationRunner(
currentGuestThreadHandle,
MapGuestThreadPriority(guestThread.Priority));
} }
else else
{ {
@@ -2990,6 +2998,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
EntryPoint = request.EntryPoint, EntryPoint = request.EntryPoint,
Argument = request.Argument, Argument = request.Argument,
Name = string.IsNullOrWhiteSpace(request.Name) ? $"Thread-{request.ThreadHandle:X}" : request.Name, Name = string.IsNullOrWhiteSpace(request.Name) ? $"Thread-{request.ThreadHandle:X}" : request.Name,
Priority = request.Priority,
AffinityMask = request.AffinityMask,
Context = context, Context = context,
State = GuestThreadRunState.Ready, State = GuestThreadRunState.Ready,
}; };
@@ -3129,11 +3139,77 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
context.TryWriteUInt64(tlsBase + 0x60, tlsBase); 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) private void RunGuestThread(GuestThreadState thread, string reason)
{ {
var previousLastError = LastError; var previousLastError = LastError;
var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(thread.ThreadHandle); var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(thread.ThreadHandle);
var previousGuestThreadState = _activeGuestThreadState; var previousGuestThreadState = _activeGuestThreadState;
ApplyGuestThreadAffinity(thread.AffinityMask);
Volatile.Write(ref thread.HostThreadId, unchecked((int)GetCurrentThreadId())); Volatile.Write(ref thread.HostThreadId, unchecked((int)GetCurrentThreadId()));
_activeGuestThreadState = thread; _activeGuestThreadState = thread;
try try
@@ -3183,6 +3259,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
case GuestNativeCallExitReason.Blocked: case GuestNativeCallExitReason.Blocked:
thread.State = GuestThreadRunState.Blocked; thread.State = GuestThreadRunState.Blocked;
thread.BlockReason = blockReason; 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; break;
default: default:
thread.State = GuestThreadRunState.Faulted; thread.State = GuestThreadRunState.Faulted;
@@ -4144,6 +4231,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
[DllImport("kernel32.dll")] [DllImport("kernel32.dll")]
private static extern uint GetCurrentThreadId(); 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)] [DllImport("kernel32.dll", SetLastError = true)]
private static extern nint OpenThread(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId); private static extern nint OpenThread(uint dwDesiredAccess, [MarshalAs(UnmanagedType.Bool)] bool bInheritHandle, uint dwThreadId);
+231 -67
View File
@@ -9,7 +9,7 @@ namespace SharpEmu.Core.Memory;
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable 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 object _guestAllocationGate = new();
private readonly List<MemoryRegion> _regions = new(); private readonly List<MemoryRegion> _regions = new();
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new(); private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
@@ -78,7 +78,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return false; return false;
} }
lock (_gate) _gate.EnterWriteLock();
try
{ {
_regions.Add(new MemoryRegion _regions.Add(new MemoryRegion
{ {
@@ -89,6 +90,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
Protection = protection Protection = protection
}); });
} }
finally
{
_gate.ExitWriteLock();
}
var allocationKind = executable ? "executable memory" : "data memory"; var allocationKind = executable ? "executable memory" : "data memory";
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)"); 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 _regions.Add(new MemoryRegion
{ {
@@ -213,6 +219,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
Protection = protection Protection = protection
}); });
} }
finally
{
_gate.ExitWriteLock();
}
var allocationKind = reservedOnly var allocationKind = reservedOnly
? "reserved data memory (lazy commit)" ? "reserved data memory (lazy commit)"
@@ -315,7 +325,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
{ {
lock (_guestAllocationGate) lock (_guestAllocationGate)
{ {
lock (_gate) _gate.EnterWriteLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -324,6 +335,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_regions.Clear(); _regions.Clear();
_pageProtections.Clear(); _pageProtections.Clear();
} }
finally
{
_gate.ExitWriteLock();
}
_guestAllocationArenaBase = 0; _guestAllocationArenaBase = 0;
_guestAllocationOffset = 0; _guestAllocationOffset = 0;
@@ -343,7 +358,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var mapEnd = AlignUp(segmentEnd, PageSize); var mapEnd = AlignUp(segmentEnd, PageSize);
var mapSize = checked(mapEnd - mapStart); var mapSize = checked(mapEnd - mapStart);
lock (_gate) _gate.EnterWriteLock();
try
{ {
var existingRegion = FindRegion(mapStart, mapSize); var existingRegion = FindRegion(mapStart, mapSize);
if (existingRegion == null) 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})"); 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) private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
@@ -425,7 +445,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions() public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
{ {
lock (_gate) _gate.EnterReadLock();
try
{ {
var snapshot = new VirtualMemoryRegion[_regions.Count]; var snapshot = new VirtualMemoryRegion[_regions.Count];
for (var i = 0; i < _regions.Count; i++) for (var i = 0; i < _regions.Count; i++)
@@ -440,11 +461,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
return snapshot; return snapshot;
} }
finally
{
_gate.ExitReadLock();
}
} }
public bool TryRead(ulong virtualAddress, Span<byte> destination) public bool TryRead(ulong virtualAddress, Span<byte> destination)
{ {
lock (_gate) var requiresExclusiveAccess = false;
_gate.EnterReadLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -456,48 +483,55 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true; return true;
} }
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region)) if (region.IsReservedOnly)
{ {
return false; if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
}
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
{
fixed (byte* destPtr = destination)
{ {
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length); return false;
}
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
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; return true;
} }
} }
}
finally
{
_gate.ExitReadLock();
}
if (!requiresExclusiveAccess)
{
return false; return false;
} }
_gate.EnterWriteLock();
try
{
return TryReadExclusive(virtualAddress, destination);
}
finally
{
_gate.ExitWriteLock();
}
} }
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source) public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{ {
lock (_gate) var requiresExclusiveAccess = false;
_gate.EnterReadLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -509,47 +543,148 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true; return true;
} }
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region)) if (region.IsReservedOnly)
{ {
return false; if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
}
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
{
fixed (byte* srcPtr = source)
{ {
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length); return false;
}
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
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
{ {
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _); requiresExclusiveAccess = true;
if (IsExecutableProtection(oldProtect)) break;
{ }
FlushInstructionCache(null, destPtr, (nuint)source.Length);
} fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
} }
return true; return true;
} }
} }
}
finally
{
_gate.ExitReadLock();
}
if (!requiresExclusiveAccess)
{
return false; 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) public bool TryWriteUInt64(ulong virtualAddress, ulong value)
@@ -561,7 +696,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public void* GetPointer(ulong virtualAddress) public void* GetPointer(ulong virtualAddress)
{ {
lock (_gate) _gate.EnterReadLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -573,11 +709,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
return null; return null;
} }
finally
{
_gate.ExitReadLock();
}
} }
public bool IsAccessible(ulong virtualAddress, ulong size) public bool IsAccessible(ulong virtualAddress, ulong size)
{ {
lock (_gate) _gate.EnterReadLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -588,6 +729,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
return false; return false;
} }
finally
{
_gate.ExitReadLock();
}
} }
private MemoryRegion? FindRegion(ulong address, ulong size) private MemoryRegion? FindRegion(ulong address, ulong size)
@@ -611,7 +756,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
var end = address + size; var end = address + size;
lock (_gate) _gate.EnterReadLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -622,6 +768,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
} }
} }
finally
{
_gate.ExitReadLock();
}
return overlapEnd != 0; return overlapEnd != 0;
} }
@@ -707,15 +857,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var endPage = AlignUp(address + size, PageSize); var endPage = AlignUp(address + size, PageSize);
var commitProtection = GetCommitProtection(region); 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) if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
{ {
return false; 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) if (info.State == MEM_COMMIT)
{ {
pageAddress = rangeEnd;
continue; continue;
} }
@@ -724,10 +885,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return false; 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; return false;
} }
pageAddress = rangeEnd;
} }
return true; return true;
@@ -10,6 +10,7 @@ using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut; using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel; using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent; using SharpEmu.Libs.AppContent;
using SharpEmu.Libs.SaveData;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -136,6 +137,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
KernelModuleRegistry.Reset(); KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath); var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version); VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false); RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress); KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}"); Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
+3 -1
View File
@@ -10,7 +10,9 @@ public readonly record struct GuestThreadStartRequest(
ulong EntryPoint, ulong EntryPoint,
ulong Argument, ulong Argument,
ulong AttributeAddress, ulong AttributeAddress,
string Name); string Name,
int Priority,
ulong AffinityMask);
public readonly record struct GuestThreadSnapshot( public readonly record struct GuestThreadSnapshot(
ulong ThreadHandle, ulong ThreadHandle,
+57 -27
View File
@@ -77,12 +77,7 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
var buffer = 0UL; if (!InitializeCommandBuffer(ctx, commandBuffer, buffer: 0, size: 0, aux0, aux1, clear: false))
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))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -106,8 +101,9 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
if (!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, 0) || Span<byte> auxiliaryPointers = stackalloc byte[sizeof(ulong) * 2];
!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, 0)) auxiliaryPointers.Clear();
if (!ctx.Memory.TryWrite(commandBuffer + CommandBufferAux0Offset, auxiliaryPointers))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -181,8 +177,15 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
} }
if (!ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out var buffer) || Span<byte> bufferPointers = stackalloc byte[sizeof(ulong) * 2];
!ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out var size) || 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)) !WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -476,20 +479,34 @@ public static class AmprExports
ulong aux1, ulong aux1,
bool clear) bool clear)
{ {
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
if (clear) if (clear)
{ {
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
header.Clear(); header.Clear();
if (!ctx.Memory.TryWrite(commandBuffer, header)) }
else
{
if (!ctx.Memory.TryRead(commandBuffer, header))
{ {
return false; return false;
} }
buffer = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferDataOffset..]);
size = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferSizeOffset..]);
} }
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) && BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSelfOffset..], commandBuffer);
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, aux0) && BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferDataOffset..], buffer);
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, aux1) && BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSizeOffset..], size);
WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0); 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) private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
@@ -504,6 +521,26 @@ public static class AmprExports
return false; 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()); var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
lock (state) lock (state)
{ {
@@ -511,15 +548,6 @@ public static class AmprExports
state.Size = size; state.Size = size;
state.WriteOffset = writeOffset; 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( private static bool TryGetCommandBufferState(
@@ -540,9 +568,11 @@ public static class AmprExports
return true; return true;
} }
if (ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer) && Span<byte> pointers = stackalloc byte[sizeof(ulong) * 2];
ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size)) if (ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, pointers))
{ {
buffer = BinaryPrimitives.ReadUInt64LittleEndian(pointers);
size = BinaryPrimitives.ReadUInt64LittleEndian(pointers[sizeof(ulong)..]);
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState()); state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
lock (state) lock (state)
{ {
+21 -2
View File
@@ -204,6 +204,16 @@ public static class KernelExports
var nameAddress = ctx[CpuRegister.R8]; var nameAddress = ctx[CpuRegister.R8];
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256); var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
var threadHandle = KernelPthreadState.CreateThreadHandle(name); 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)) if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, threadHandle))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -212,13 +222,22 @@ public static class KernelExports
if (ShouldTracePthread()) if (ShouldTracePthread())
{ {
Console.Error.WriteLine( 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; var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is not null && entryAddress != 0) 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)) if (!scheduler.TryStartThread(ctx, request, out var error))
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
@@ -602,9 +602,7 @@ public static class KernelMemoryCompatExports
} }
else else
{ {
ulong NextGpArg() => vaCursor.NextGpArg(); rendered = FormatString(ctx, format, ref vaCursor);
double NextFloatArg() => vaCursor.NextFloatArg();
rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
} }
Console.Write(rendered); Console.Write(rendered);
@@ -2908,9 +2906,8 @@ public static class KernelMemoryCompatExports
var format = Encoding.UTF8.GetString(formatBytes); var format = Encoding.UTF8.GetString(formatBytes);
var result = FormatString(ctx, format); 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) private static int VsnprintfCore(CpuContext ctx)
@@ -2931,12 +2928,9 @@ public static class KernelMemoryCompatExports
return WriteSnprintfOutput(ctx, destination, bufferSize, formatBytes); return WriteSnprintfOutput(ctx, destination, bufferSize, formatBytes);
} }
ulong NextGpArg() => vaCursor.NextGpArg(); var rendered = FormatString(ctx, format, ref vaCursor);
double NextFloatArg() => vaCursor.NextFloatArg();
var rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
var outputBytes = Encoding.UTF8.GetBytes(rendered); return WriteSnprintfOutput(ctx, destination, bufferSize, rendered);
return WriteSnprintfOutput(ctx, destination, bufferSize, outputBytes);
} }
private static int SwprintfCore(CpuContext ctx) private static int SwprintfCore(CpuContext ctx)
@@ -2977,9 +2971,7 @@ public static class KernelMemoryCompatExports
else else
{ {
TraceWidePrintfVaList(ctx, "vswprintf", format, vaListAddress, vaCursor); TraceWidePrintfVaList(ctx, "vswprintf", format, vaListAddress, vaCursor);
ulong NextGpArg() => vaCursor.NextGpArg(); rendered = FormatString(ctx, format, ref vaCursor);
double NextFloatArg() => vaCursor.NextFloatArg();
rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
} }
TraceWidePrintf(ctx, "vswprintf", destination, bufferSize, format, rendered); TraceWidePrintf(ctx, "vswprintf", destination, bufferSize, format, rendered);
@@ -2994,10 +2986,8 @@ public static class KernelMemoryCompatExports
return false; return false;
} }
if (!TryReadUInt32Compat(ctx, vaListAddress + 0, out var gpOffset) || Span<byte> vaList = stackalloc byte[24];
!TryReadUInt32Compat(ctx, vaListAddress + 4, out var fpOffset) || if (!TryReadCompat(ctx, vaListAddress, vaList))
!TryReadUInt64Compat(ctx, vaListAddress + 8, out var overflowArgArea) ||
!TryReadUInt64Compat(ctx, vaListAddress + 16, out var regSaveArea))
{ {
return false; return false;
} }
@@ -3005,10 +2995,10 @@ public static class KernelMemoryCompatExports
cursor = new SysVAmd64VaListCursor( cursor = new SysVAmd64VaListCursor(
ctx, ctx,
vaListAddress, vaListAddress,
gpOffset, BinaryPrimitives.ReadUInt32LittleEndian(vaList),
fpOffset, BinaryPrimitives.ReadUInt32LittleEndian(vaList[4..]),
overflowArgArea, BinaryPrimitives.ReadUInt64LittleEndian(vaList[8..]),
regSaveArea); BinaryPrimitives.ReadUInt64LittleEndian(vaList[16..]));
return true; return true;
} }
@@ -3038,6 +3028,21 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( private static int WriteSwprintfOutput(
CpuContext ctx, CpuContext ctx,
ulong destination, ulong destination,
@@ -3329,30 +3334,8 @@ public static class KernelMemoryCompatExports
internal static string FormatStringFromVarArgs(CpuContext ctx, string format, int firstGpArgIndex) internal static string FormatStringFromVarArgs(CpuContext ctx, string format, int firstGpArgIndex)
{ {
var gpIndex = Math.Max(0, firstGpArgIndex); var argumentSource = new RegisterPrintfArgumentSource(ctx, Math.Max(0, firstGpArgIndex));
return FormatString(ctx, format, ref argumentSource);
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);
} }
private static string FormatString(CpuContext ctx, string format) private static string FormatString(CpuContext ctx, string format)
@@ -3360,13 +3343,13 @@ public static class KernelMemoryCompatExports
return FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 3); return FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 3);
} }
private static string FormatString( private static string FormatString<TArgumentSource>(
CpuContext ctx, CpuContext ctx,
string format, string format,
Func<ulong> nextGpArg, ref TArgumentSource argumentSource)
Func<double> nextFloatArg) where TArgumentSource : struct, IPrintfArgumentSource
{ {
var sb = new StringBuilder(); var sb = new StringBuilder(format.Length + 32);
for (var i = 0; i < format.Length; i++) for (var i = 0; i < format.Length; i++)
{ {
@@ -3405,7 +3388,7 @@ public static class KernelMemoryCompatExports
var width = 0; var width = 0;
if (i < format.Length && format[i] == '*') if (i < format.Length && format[i] == '*')
{ {
width = unchecked((int)nextGpArg()); width = unchecked((int)argumentSource.NextGpArg());
i++; i++;
if (width < 0) if (width < 0)
{ {
@@ -3428,7 +3411,7 @@ public static class KernelMemoryCompatExports
i++; i++;
if (i < format.Length && format[i] == '*') if (i < format.Length && format[i] == '*')
{ {
precision = unchecked((int)nextGpArg()); precision = unchecked((int)argumentSource.NextGpArg());
i++; i++;
} }
else if (i < format.Length && char.IsDigit(format[i])) else if (i < format.Length && char.IsDigit(format[i]))
@@ -3482,14 +3465,14 @@ public static class KernelMemoryCompatExports
{ {
long value = lengthMod switch long value = lengthMod switch
{ {
"hh" => unchecked((sbyte)nextGpArg()), "hh" => unchecked((sbyte)argumentSource.NextGpArg()),
"h" => unchecked((short)nextGpArg()), "h" => unchecked((short)argumentSource.NextGpArg()),
"l" => unchecked((long)nextGpArg()), "l" => unchecked((long)argumentSource.NextGpArg()),
"ll" => unchecked((long)nextGpArg()), "ll" => unchecked((long)argumentSource.NextGpArg()),
"j" => unchecked((long)nextGpArg()), "j" => unchecked((long)argumentSource.NextGpArg()),
"z" => unchecked((long)nextGpArg()), "z" => unchecked((long)argumentSource.NextGpArg()),
"t" => unchecked((long)nextGpArg()), "t" => unchecked((long)argumentSource.NextGpArg()),
_ => unchecked((int)nextGpArg()) _ => unchecked((int)argumentSource.NextGpArg())
}; };
var formatted = value.ToString(); var formatted = value.ToString();
@@ -3506,14 +3489,14 @@ public static class KernelMemoryCompatExports
{ {
ulong value = lengthMod switch ulong value = lengthMod switch
{ {
"hh" => (byte)nextGpArg(), "hh" => (byte)argumentSource.NextGpArg(),
"h" => (ushort)nextGpArg(), "h" => (ushort)argumentSource.NextGpArg(),
"l" => nextGpArg(), "l" => argumentSource.NextGpArg(),
"ll" => nextGpArg(), "ll" => argumentSource.NextGpArg(),
"j" => nextGpArg(), "j" => argumentSource.NextGpArg(),
"z" => nextGpArg(), "z" => argumentSource.NextGpArg(),
"t" => nextGpArg(), "t" => argumentSource.NextGpArg(),
_ => (uint)nextGpArg() _ => (uint)argumentSource.NextGpArg()
}; };
var formatted = value.ToString(); var formatted = value.ToString();
@@ -3526,14 +3509,14 @@ public static class KernelMemoryCompatExports
{ {
ulong value = lengthMod switch ulong value = lengthMod switch
{ {
"hh" => (byte)nextGpArg(), "hh" => (byte)argumentSource.NextGpArg(),
"h" => (ushort)nextGpArg(), "h" => (ushort)argumentSource.NextGpArg(),
"l" => nextGpArg(), "l" => argumentSource.NextGpArg(),
"ll" => nextGpArg(), "ll" => argumentSource.NextGpArg(),
"j" => nextGpArg(), "j" => argumentSource.NextGpArg(),
"z" => nextGpArg(), "z" => argumentSource.NextGpArg(),
"t" => nextGpArg(), "t" => argumentSource.NextGpArg(),
_ => (uint)nextGpArg() _ => (uint)argumentSource.NextGpArg()
}; };
var formatted = specifier == 'x' var formatted = specifier == 'x'
@@ -3551,14 +3534,14 @@ public static class KernelMemoryCompatExports
{ {
ulong value = lengthMod switch ulong value = lengthMod switch
{ {
"hh" => (byte)nextGpArg(), "hh" => (byte)argumentSource.NextGpArg(),
"h" => (ushort)nextGpArg(), "h" => (ushort)argumentSource.NextGpArg(),
"l" => nextGpArg(), "l" => argumentSource.NextGpArg(),
"ll" => nextGpArg(), "ll" => argumentSource.NextGpArg(),
"j" => nextGpArg(), "j" => argumentSource.NextGpArg(),
"z" => nextGpArg(), "z" => argumentSource.NextGpArg(),
"t" => nextGpArg(), "t" => argumentSource.NextGpArg(),
_ => (uint)nextGpArg() _ => (uint)argumentSource.NextGpArg()
}; };
var formatted = Convert.ToString((long)value, 8); var formatted = Convert.ToString((long)value, 8);
@@ -3571,7 +3554,7 @@ public static class KernelMemoryCompatExports
case 'p': case 'p':
{ {
var value = nextGpArg(); var value = argumentSource.NextGpArg();
var formatted = value == 0 var formatted = value == 0
? "(nil)" ? "(nil)"
: $"0x{value:X}"; : $"0x{value:X}";
@@ -3581,7 +3564,7 @@ public static class KernelMemoryCompatExports
case 's': case 's':
{ {
var strAddr = nextGpArg(); var strAddr = argumentSource.NextGpArg();
TracePrintfStringArgument(ctx, lengthMod, strAddr); TracePrintfStringArgument(ctx, lengthMod, strAddr);
if (strAddr == 0) if (strAddr == 0)
{ {
@@ -3620,14 +3603,14 @@ public static class KernelMemoryCompatExports
string renderedChar; string renderedChar;
if (lengthMod == "l") if (lengthMod == "l")
{ {
var scalar = unchecked((ushort)nextGpArg()); var scalar = unchecked((ushort)argumentSource.NextGpArg());
renderedChar = TryConvertWideScalarToString(scalar, out var wideCharText) renderedChar = TryConvertWideScalarToString(scalar, out var wideCharText)
? wideCharText ? wideCharText
: "?"; : "?";
} }
else else
{ {
renderedChar = ((char)(byte)nextGpArg()).ToString(); renderedChar = ((char)(byte)argumentSource.NextGpArg()).ToString();
} }
sb.Append(PadString(renderedChar, width, leftAlign, false)); sb.Append(PadString(renderedChar, width, leftAlign, false));
@@ -3641,7 +3624,7 @@ public static class KernelMemoryCompatExports
case 'g': case 'g':
case 'G': case 'G':
{ {
var value = nextFloatArg(); var value = argumentSource.NextFloatArg();
var formatStr = precision >= 0 var formatStr = precision >= 0
? $"{{0:{specifier}{precision}}}" ? $"{{0:{specifier}{precision}}}"
@@ -3659,7 +3642,7 @@ public static class KernelMemoryCompatExports
case 'n': case 'n':
{ {
var addr = nextGpArg(); var addr = argumentSource.NextGpArg();
if (addr != 0) if (addr != 0)
{ {
_ = TryWriteInt32(ctx, addr, sb.Length); _ = TryWriteInt32(ctx, addr, sb.Length);
@@ -3699,7 +3682,46 @@ public static class KernelMemoryCompatExports
return leftAlign ? str + padding : padding + str; 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 GpSaveAreaLimit = 48;
private const uint FpSaveAreaLimit = 176; private const uint FpSaveAreaLimit = 176;
@@ -4297,9 +4319,32 @@ public static class KernelMemoryCompatExports
} }
const int maxChunkSize = 4096; 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 chunk = GC.AllocateUninitializedArray<byte>(Math.Min(maxChunkSize, limit));
var writer = new ArrayBufferWriter<byte>(Math.Min(limit, 256)); 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) while (offset < (ulong)limit)
{ {
var current = address + offset; var current = address + offset;
@@ -13,13 +13,13 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelPthreadExtendedCompatExports public static class KernelPthreadExtendedCompatExports
{ {
private const int DefaultThreadPriority = 700; private const int DefaultThreadPriority = 700;
private const ulong DefaultThreadAffinityMask = ulong.MaxValue; private const ulong DefaultThreadAffinityMask = 0x7FUL;
private const int DefaultDetachState = 0; private const int DefaultDetachState = 0;
private const ulong DefaultGuardSize = 0x1000UL; private const ulong DefaultGuardSize = 0x1000UL;
private const ulong DefaultStackSize = 0x1_00000UL; private const ulong DefaultStackSize = 0x1_00000UL;
private const int DefaultInheritSched = 0; private const int DefaultInheritSched = 4;
private const int DefaultSchedPolicy = 0; private const int DefaultSchedPolicy = 1;
private const int DefaultSchedPriority = 0; private const int DefaultSchedPriority = DefaultThreadPriority;
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000; private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_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(); 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 private sealed class ThreadState
{ {
public string Name { get; set; } = string.Empty; public string Name { get; set; } = string.Empty;
@@ -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 // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut;
using System.Buffers.Binary; using System.Buffers.Binary;
namespace SharpEmu.Libs.SystemService; namespace SharpEmu.Libs.SystemService;
@@ -85,6 +86,17 @@ public static class SystemServiceExports
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : 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) private static int SetReturn(CpuContext ctx, int result)
{ {
ctx[CpuRegister.Rax] = unchecked((ulong)result); ctx[CpuRegister.Rax] = unchecked((ulong)result);
@@ -26,6 +26,7 @@ internal static unsafe class VulkanVideoPresenter
private static uint _windowWidth; private static uint _windowWidth;
private static uint _windowHeight; private static uint _windowHeight;
private static bool _closed; private static bool _closed;
private static bool _splashHidden;
public static void EnsureStarted(uint width, uint height) public static void EnsureStarted(uint width, uint height)
{ {
@@ -55,7 +56,15 @@ internal static unsafe class VulkanVideoPresenter
_windowWidth = width; _windowWidth = width;
_windowHeight = height; _windowHeight = height;
_latestPresentation ??= hasSplash _latestPresentation ??= _splashHidden
? new Presentation(
CreateBlackFrame(width, height),
width,
height,
1,
GuestDrawKind.None,
IsSplash: false)
: hasSplash
? new Presentation( ? new Presentation(
splashPixels, splashPixels,
splashWidth, 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) public static void Submit(byte[] bgraFrame, uint width, uint height)
{ {
if (bgraFrame.Length != checked((int)(width * height * 4))) 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() private static void Run()
{ {
uint width; uint width;
@@ -259,6 +308,8 @@ internal static unsafe class VulkanVideoPresenter
options.Title = VideoOutExports.GetWindowTitle(); options.Title = VideoOutExports.GetWindowTitle();
options.WindowBorder = WindowBorder.Fixed; options.WindowBorder = WindowBorder.Fixed;
options.VSync = true; options.VSync = true;
options.FramesPerSecond = 60;
options.UpdatesPerSecond = 60;
_window = Window.Create(options); _window = Window.Create(options);
_window.Load += Initialize; _window.Load += Initialize;
_window.Render += Render; _window.Render += Render;