Compare commits

...

15 Commits

Author SHA1 Message Date
ParantezTech daeff21516 [readme] update screenshots 2026-06-29 14:34:36 +03:00
ParantezTech 82619deb34 [ci] ignore image files from actions 2026-06-29 14:32:46 +03:00
ParantezTech 697ad7be80 [libs] Add NP entitlement validation 2026-06-29 14:31:37 +03:00
ParantezTech 73c987e49f [libs] Improve video output handling 2026-06-29 14:31:26 +03:00
ParantezTech cd79275117 [libs] Enhance PlayGo streaming service 2026-06-29 14:31:18 +03:00
ParantezTech b14ecae504 [core] Refine native CPU execution imports 2026-06-29 14:31:10 +03:00
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
18 changed files with 1230 additions and 199 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 621 KiB

+6
View File
@@ -9,11 +9,17 @@ on:
- "**"
paths-ignore:
- "**/*.md"
- "**/*.png"
- "**/*.jpeg"
- "**/*.jpg"
pull_request:
branches:
- main
paths-ignore:
- "**/*.md"
- "**/*.png"
- "**/*.jpeg"
- "**/*.jpg"
workflow_dispatch:
permissions:
BIN
View File
Binary file not shown.
@@ -527,7 +527,10 @@ 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(
@@ -635,6 +638,7 @@ public sealed partial class DirectExecutionBackend
"7H0iTOciTLo" or
"2Z+PpY6CaJg" or
"8aI7R7WaOlc" or
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
"a8uLzYY--tM" or
"Qs1xtplKo0U" or
"GuchCTefuZw" or
@@ -653,7 +657,13 @@ public sealed partial class DirectExecutionBackend
"rqwFKI4PAiM" or
"eE4Szl8sil8" or
"qvMUCyyaCSI" or
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
"TywrFKCoLGY" or // sceSaveDataInitialize3
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
"jO8DM8oyego" or // sceNpEntitlementAccessInitialize
"27bAgiJmOh0" or // pthread_cond_timedwait
"iQw3iQPhvUQ" or // sceNetCtlCheckCallback
"Q2V+iqvjgC0" or // vsnprintf
"j4ViWNHEgww" or // strlen
"5jNubw4vlAA" or // strnlen
"LHMrG7e8G78" or // wcslen
@@ -347,6 +347,10 @@ 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; }
@@ -388,14 +392,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();
}
@@ -2393,7 +2397,9 @@ 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;
}
@@ -2448,7 +2454,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 +2857,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 +2998,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 +3139,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
@@ -3183,6 +3259,17 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
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 +4231,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);
+231 -67
View File
@@ -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}");
+3 -1
View File
@@ -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,
+57 -27
View File
@@ -77,12 +77,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 +101,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 +177,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 +479,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 +521,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 +548,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 +568,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)
{
+21 -2
View File
@@ -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(
@@ -602,9 +602,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 +2906,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 +2928,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 +2971,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 +2986,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 +2995,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 +3028,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 +3334,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 +3343,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 +3388,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 +3411,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 +3465,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 +3489,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 +3509,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 +3534,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 +3554,7 @@ public static class KernelMemoryCompatExports
case 'p':
{
var value = nextGpArg();
var value = argumentSource.NextGpArg();
var formatted = value == 0
? "(nil)"
: $"0x{value:X}";
@@ -3581,7 +3564,7 @@ public static class KernelMemoryCompatExports
case 's':
{
var strAddr = nextGpArg();
var strAddr = argumentSource.NextGpArg();
TracePrintfStringArgument(ctx, lengthMod, strAddr);
if (strAddr == 0)
{
@@ -3620,14 +3603,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 +3624,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 +3642,7 @@ public static class KernelMemoryCompatExports
case 'n':
{
var addr = nextGpArg();
var addr = argumentSource.NextGpArg();
if (addr != 0)
{
_ = TryWriteInt32(ctx, addr, sb.Length);
@@ -3699,7 +3682,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;
@@ -4297,9 +4319,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;
@@ -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,51 @@
// 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;
[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);
}
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}");
}
}
+43
View File
@@ -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,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;