Compare commits

..

20 Commits

Author SHA1 Message Date
ParantezTech af35389bfd [libs] Add audio and system gesture exports 2026-06-28 23:47:00 +03:00
ParantezTech 4ae778ef39 [libs] Update system and video exports 2026-06-28 23:46:56 +03:00
ParantezTech a04e70c0b3 [libs] Update NP and pad exports 2026-06-28 23:46:56 +03:00
ParantezTech 02222c7919 [libs] Update network exports 2026-06-28 23:46:56 +03:00
ParantezTech 46b3a207c1 [libs] Update AMPR and PlayGo exports 2026-06-28 23:46:55 +03:00
ParantezTech 418d46beb5 [libs] Update AGC and fiber exports 2026-06-28 23:46:44 +03:00
ParantezTech 0e922a73ee [core] Improve native execution and guest threading 2026-06-28 23:45:26 +03:00
ParantezTech a5172fd2c0 [kernel] Guest-thread blocking for pthread_mutex_lock is currently disabled 2026-06-28 23:45:05 +03:00
ParantezTech 4ab614e68a [npManager] initial Network Platform implement (just set game title etc.) 2026-06-23 19:30:27 +03:00
ParantezTech e581fe41f4 [kernel] pthread improvements 2026-06-23 19:29:29 +03:00
ParantezTech 0f0ec9a020 [appContent] correct way to get metadata from game 2026-06-23 19:28:53 +03:00
ParantezTech 70048cb49f [agc] fix shader invalid argument 2026-06-23 19:27:57 +03:00
ParantezTech 4777ec4544 [sceShare] initial share exports 2026-06-23 19:27:24 +03:00
ParantezTech caee2ad692 [readme] alignment 2026-06-23 17:54:39 +03:00
ParantezTech 7c83f7f925 [readme] logo align 2026-06-23 17:52:45 +03:00
ParantezTech 0e37bc95e9 upload logo 2026-06-23 17:51:28 +03:00
ParantezTech 318425630a [readme] edit for new milestones 2026-06-23 16:06:58 +03:00
ParantezTech 992ef68ba8 [readme] update 2026-06-23 16:05:16 +03:00
ParantezTech 01ebab90c0 [playgo] In some games, chunk scenarios assertion rewrite 2026-06-23 15:50:43 +03:00
ParantezTech d134f9b9f6 [fiber] synchronization problems have been fixed for such a titles: Demon's Souls
[ampr] new exports
[memory] trampoline fixes
2026-06-23 15:48:45 +03:00
39 changed files with 6882 additions and 737 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

BIN
View File
Binary file not shown.
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
@@ -12,10 +13,23 @@ namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
private void RecordRecentImportTrace(string traceLine)
private void RecordRecentImportTrace(
long dispatchIndex,
string nid,
ulong returnRip,
ulong arg0,
ulong arg1,
ulong arg2)
{
_recentImportTrace[_recentImportTraceWriteIndex] = traceLine;
_recentImportTrace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
dispatchIndex,
nid,
returnRip,
arg0,
arg1,
arg2);
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
if (_recentImportTraceCount < _recentImportTrace.Length)
{
@@ -34,10 +48,12 @@ public sealed partial class DirectExecutionBackend
for (int i = 0; i < _recentImportTraceCount; i++)
{
int num2 = (num + i) % _recentImportTrace.Length;
string text = _recentImportTrace[num2];
if (!string.IsNullOrEmpty(text))
var entry = _recentImportTrace[num2];
if (!string.IsNullOrEmpty(entry.Nid))
{
Console.Error.WriteLine("[LOADER][INFO] " + text);
Console.Error.WriteLine(
$"[LOADER][INFO] #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
}
}
}
@@ -302,11 +318,24 @@ public sealed partial class DirectExecutionBackend
private unsafe static bool IsExecutableAddress(ulong address)
{
var pageAddress = address & ~0xFFFUL;
if (_knownExecutablePages.ContainsKey(pageAddress))
{
return true;
}
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
return lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
if (executable)
{
_knownExecutablePages.TryAdd(pageAddress, 0);
}
return executable;
}
private static ulong AlignUp(ulong value, ulong alignment)
@@ -15,11 +15,19 @@ namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
private static int _lazyCommitTraceCount;
private unsafe void SetupExceptionHandler()
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
{
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, RawVectoredHandlerPtrManaged);
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
if (_rawExceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
}
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
}
else
@@ -29,12 +37,22 @@ public sealed partial class DirectExecutionBackend
_handlerDelegate = VectoredHandler;
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
if (_exceptionHandlerStub == 0)
{
throw new InvalidOperationException("Failed to create exception handler trampoline");
}
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
_unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
SetUnhandledExceptionFilter(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
if (_unhandledFilterStub == 0)
{
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
}
SetUnhandledExceptionFilter(_unhandledFilterStub);
}
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
@@ -896,7 +914,21 @@ public sealed partial class DirectExecutionBackend
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
}
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
{
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
}
return true;
}
bool committed = false;
ulong committedBase = 0;
@@ -904,14 +936,25 @@ public sealed partial class DirectExecutionBackend
if (mbi.State == 65536)
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
{
committed = true;
committedBase = largeBase;
committedSize = 2097152uL;
committedBase = windowBase;
committedSize = windowSize;
}
else
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeBase;
committedSize = 2097152uL;
}
}
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
@@ -934,7 +977,10 @@ public sealed partial class DirectExecutionBackend
}
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
}
return true;
}
@@ -943,14 +989,25 @@ public sealed partial class DirectExecutionBackend
return false;
}
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
committedSize = 2097152uL;
committedBase = commitWindowBase;
committedSize = commitWindowSize;
}
else
{
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
committedSize = 2097152uL;
}
}
if (!committed)
{
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
@@ -979,9 +1036,46 @@ public sealed partial class DirectExecutionBackend
}
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
if (traceLazyCommit)
{
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
}
return true;
static bool TryGetLazyCommitWindow(ulong fault, ulong regionBase, ulong regionSize, out ulong baseAddress, out ulong length)
{
baseAddress = 0;
length = 0;
if (regionSize == 0 || ulong.MaxValue - regionBase < regionSize)
{
return false;
}
ulong regionEnd = regionBase + regionSize;
ulong windowBase = fault & ~(LazyCommitWindowBytes - 1);
if (windowBase < regionBase)
{
windowBase = regionBase;
}
if (windowBase >= regionEnd)
{
return false;
}
ulong windowEnd = Math.Min(regionEnd, windowBase + LazyCommitWindowBytes);
ulong windowSize = windowEnd - windowBase;
windowSize &= 0xFFFFFFFFFFFFF000uL;
if (windowSize == 0)
{
return false;
}
baseAddress = windowBase;
length = windowSize;
return true;
}
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
{
if (length == 0)
@@ -1008,6 +1102,49 @@ public sealed partial class DirectExecutionBackend
}
return TryCommitRange(commitAddress, commitSize, protection);
}
static bool IsAccessCompatible(ulong accessType, uint protection)
{
const uint pageNoAccess = 0x01;
const uint pageReadOnly = 0x02;
const uint pageReadWrite = 0x04;
const uint pageWriteCopy = 0x08;
const uint pageExecute = 0x10;
const uint pageExecuteRead = 0x20;
const uint pageExecuteReadWrite = 0x40;
const uint pageExecuteWriteCopy = 0x80;
const uint pageGuard = 0x100;
const uint accessMask = 0xFF;
if ((protection & pageGuard) != 0)
{
return false;
}
uint access = protection & accessMask;
if (access == pageNoAccess)
{
return false;
}
return accessType switch
{
0 => access is pageReadOnly or pageReadWrite or pageWriteCopy or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
1 => access is pageReadWrite or pageWriteCopy or pageExecuteReadWrite or pageExecuteWriteCopy,
8 => access is pageExecute or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
_ => false
};
}
}
private static bool ShouldTraceLazyCommit(int traceIndex)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LAZY_COMMIT"), "1", StringComparison.Ordinal))
{
return true;
}
return traceIndex <= 16 || traceIndex % 256 == 0;
}
private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
@@ -14,6 +14,9 @@ namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private readonly object _importResultLogSampleGate = new();
private readonly Dictionary<string, int> _importResultLogSamples = new(StringComparer.Ordinal);
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
{
try
@@ -73,8 +76,11 @@ public sealed partial class DirectExecutionBackend
private unsafe ulong DispatchImport(int importIndex, nint argPackPtr)
{
long num = Interlocked.Increment(ref _importDispatchCount);
MarkExecutionProgress();
long num = NextImportDispatchIndex();
if ((num & 0x3F) == 0)
{
MarkExecutionProgress();
}
var cpuContext = ActiveCpuContext;
if (cpuContext == null)
{
@@ -93,6 +99,12 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
_lastReportedRawSentinelRecoveries = num2;
}
if (IsLeafImport(importStubEntry.Nid) &&
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
{
return leafResult;
}
cpuContext.Rip = importStubEntry.Address;
cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
@@ -106,6 +118,10 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
cpuContext.SetXmmRegister(
0,
*(ulong*)(argPackPtr - 16),
*(ulong*)(argPackPtr - 8));
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
ulong value = cpuContext[CpuRegister.Rdi];
ulong value2 = cpuContext[CpuRegister.Rsi];
@@ -120,6 +136,7 @@ public sealed partial class DirectExecutionBackend
ulong value7 = cpuContext[CpuRegister.R14];
ulong value8 = cpuContext[CpuRegister.R15];
ulong num7 = *(ulong*)(argPackPtr + 96);
var isGuestWorker = GuestThreadExecution.IsGuestThread;
if (!IsLikelyReturnAddress(num7))
{
for (int i = 1; i <= 4; i++)
@@ -134,17 +151,29 @@ public sealed partial class DirectExecutionBackend
}
}
}
TrackDistinctImportNid(importStubEntry.Nid);
var probeImportReturn = Environment.GetEnvironmentVariable("SHARPEMU_PROBE_IMPORT_RET");
if (!string.IsNullOrWhiteSpace(probeImportReturn) &&
(string.Equals(probeImportReturn, "*", StringComparison.Ordinal) ||
string.Equals(probeImportReturn, importStubEntry.Nid, StringComparison.Ordinal)))
if (_activeGuestThreadState is { } activeGuestThreadState)
{
Interlocked.Increment(ref activeGuestThreadState.ImportCount);
Volatile.Write(ref activeGuestThreadState.LastImportNid, importStubEntry.Nid);
Volatile.Write(ref activeGuestThreadState.LastReturnRip, num7);
}
if (_logStrlenBursts)
{
TrackDistinctImportNid(importStubEntry.Nid);
TrackStrlenPrelude(importStubEntry.Nid, num, num7);
}
if (!string.IsNullOrWhiteSpace(_probeImportReturn) &&
(string.Equals(_probeImportReturn, "*", StringComparison.Ordinal) ||
string.Equals(_probeImportReturn, importStubEntry.Nid, StringComparison.Ordinal)))
{
ProbeReturnRip(num7, num);
}
TrackStrlenPrelude(importStubEntry.Nid, num, num7);
bool logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal);
if (logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
if (_logGuestContext)
{
TraceGuestContext(
$"import dispatch={num} nid={importStubEntry.Nid} ret=0x{num7:X16} managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16} active={HasActiveExecutionThread}");
}
if (_logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{
string symbolText = "<unreadable>";
if (TryReadAsciiZ(value2, 256, out var sym))
@@ -154,7 +183,10 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
}
if (!ActiveForcedGuestExit && ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) && TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
if (!isGuestWorker &&
!ActiveForcedGuestExit &&
ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) &&
TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
{
cpuContext[CpuRegister.Rax] = 1uL;
return 1uL;
@@ -163,27 +195,33 @@ public sealed partial class DirectExecutionBackend
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
bool flag3 = num >= 1020 && num <= 1040;
bool logAllImports = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_ALL_IMPORTS"), "1", StringComparison.Ordinal);
string importFilter = Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FILTER");
bool flag4 = !string.IsNullOrWhiteSpace(importFilter);
bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
bool flag5 = false;
ExportedFunction matchedExport = null;
if (_moduleManager.TryGetExport(importStubEntry.Nid, out ExportedFunction export))
ExportedFunction? matchedExport = importStubEntry.Export;
bool periodicTrace = num <= 128 ||
(num >= 240 && num <= 400) ||
(num >= 900 && num <= 1300) ||
num % 100000 == 0L ||
(importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) ||
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) ||
flag ||
flag2 ||
flag3;
if (matchedExport is not null)
{
matchedExport = export;
if (flag4)
{
flag5 = export.LibraryName.Contains(importFilter, StringComparison.OrdinalIgnoreCase)
|| export.Name.Contains(importFilter, StringComparison.OrdinalIgnoreCase)
|| importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase);
flag5 = matchedExport.LibraryName.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase)
|| matchedExport.Name.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase)
|| importStubEntry.Nid.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase);
}
}
else if (flag4)
{
flag5 = importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase);
flag5 = importStubEntry.Nid.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase);
}
bool flag6 = logAllImports || flag5;
if (!flag0 && (flag6 || num <= 128 || (num >= 240 && num <= 400) || (num >= 900 && num <= 1300) || num % 100000 == 0L || (importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) || (importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) || flag || flag2 || flag3))
bool flag6 = _logAllImports || flag5;
if (!flag0 && (flag6 || periodicTrace))
{
if (matchedExport != null)
{
@@ -216,9 +254,15 @@ public sealed partial class DirectExecutionBackend
Console.Error.Flush();
}
}
if (!flag0)
if (!flag0 && !isGuestWorker)
{
RecordRecentImportTrace($"#{num} nid={importStubEntry.Nid} ret=0x{num7:X16} rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16}");
RecordRecentImportTrace(
num,
importStubEntry.Nid,
num7,
cpuContext[CpuRegister.Rdi],
cpuContext[CpuRegister.Rsi],
cpuContext[CpuRegister.Rdx]);
}
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
{
@@ -245,11 +289,11 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine($"[LOADER][TRACE] ImportStack#{num}: rsp=0x{num9:X16} [0]=0x{value9:X16} [8]=0x{value10:X16} [10]=0x{value11:X16} [18]=0x{value12:X16} [20]=0x{value13:X16} [28]=0x{value14:X16} [30]=0x{value15:X16} [38]=0x{value16:X16} [40]=0x{value17:X16}");
}
}
if (flag6 && string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FRAMES"), "1", StringComparison.Ordinal))
if (flag6 && _logImportFrames)
{
TraceImportFrameChain(cpuContext, num);
}
if (flag6 && string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_RECENT"), "1", StringComparison.Ordinal))
if (flag6 && _logImportRecent)
{
DumpRecentImportTrace();
}
@@ -260,7 +304,7 @@ public sealed partial class DirectExecutionBackend
}
if (importStubEntry.Nid == "Ou3iL1abvng")
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STACK_CHK"), "1", StringComparison.Ordinal))
if (_logStackCheck)
{
var savedGuardAddress = value4 >= 0x10 ? value4 - 0x10 : 0;
var guardKnown = TryReadUInt64Compat(value3, out var guardValue);
@@ -279,13 +323,15 @@ public sealed partial class DirectExecutionBackend
catch
{
}
TryBypassStackChkFailTrap(num, num7);
}
try
{
OrbisGen2Result orbisGen2Result;
bool dispatchResolved = true;
var previousImportCallFrame = GuestThreadExecution.EnterImportCallFrame(num7, (ulong)argPackPtr + 104uL);
var previousImportCallFrame = GuestThreadExecution.EnterImportCallFrame(
num7,
(ulong)argPackPtr + 104uL,
ActiveGuestReturnSlotAddress);
try
{
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
@@ -296,9 +342,22 @@ public sealed partial class DirectExecutionBackend
{
orbisGen2Result = DispatchKernelDynlibDlsym();
}
else if (importStubEntry.Export is { } cachedExport &&
(cachedExport.Target & cpuContext.TargetGeneration) != 0)
{
cpuContext.ClearRaxWriteFlag();
var returnValue = cachedExport.Function(cpuContext);
if (!cpuContext.WasRaxWritten)
{
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
}
orbisGen2Result = (OrbisGen2Result)returnValue;
}
else
{
dispatchResolved = _moduleManager.TryDispatch(importStubEntry.Nid, cpuContext, out orbisGen2Result);
dispatchResolved = false;
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
}
}
finally
@@ -314,7 +373,9 @@ public sealed partial class DirectExecutionBackend
if (!dispatchResolved)
{
LastError = "Missing HLE export for NID: " + importStubEntry.Nid;
Console.Error.WriteLine($"[LOADER][WARN] Import#{num} unresolved: nid={importStubEntry.Nid} ret=0x{num7:X16}");
Console.Error.WriteLine(
$"[LOADER][WARN] Import#{num} unresolved: nid={importStubEntry.Nid} ret=0x{num7:X16} " +
$"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} r8=0x{num5:X16} r9=0x{num6:X16}");
if (importStubEntry.Nid == "L-Q3LEjIbgA")
{
string value18 = string.Join(" ", importStubEntry.Nid.Select(delegate (char c)
@@ -333,9 +394,12 @@ public sealed partial class DirectExecutionBackend
}
else if (orbisGen2Result != OrbisGen2Result.ORBIS_GEN2_OK)
{
Console.Error.WriteLine(
$"[LOADER][WARN] Import#{num} result: {orbisGen2Result} ({importStubEntry.Nid}) " +
$"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} ret=0x{num7:X16}");
if (ShouldLogImportResult(importStubEntry.Nid, orbisGen2Result))
{
Console.Error.WriteLine(
$"[LOADER][WARN] Import#{num} result: {orbisGen2Result} ({importStubEntry.Nid}) " +
$"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} ret=0x{num7:X16}");
}
}
cpuContext[CpuRegister.Rbx] = value3;
cpuContext[CpuRegister.Rbp] = value4;
@@ -345,11 +409,36 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.R15] = value8;
cpuContext[CpuRegister.Rdi] = value;
cpuContext[CpuRegister.Rsi] = value2;
if (GuestThreadExecution.TryConsumeCurrentEntryExit(out var exitStatus, out var exitReason))
if (GuestThreadExecution.TryConsumeCurrentContextTransfer(out var transferTarget))
{
if (TryCompleteGuestEntryToHostStub(argPackPtr, num, num7, importStubEntry.Nid, exitReason, exitStatus))
if (!TryPrepareGuestContextTransfer(
transferTarget,
out var transferFrame,
out var transferStub,
out var transferError))
{
cpuContext[CpuRegister.Rax] = unchecked((ulong)exitStatus);
LastError = transferError ?? "failed to prepare guest context transfer";
ActiveForcedGuestExit = true;
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
return cpuContext[CpuRegister.Rax];
}
*(ulong*)(argPackPtr + 96) = unchecked((ulong)transferStub);
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] fiber.context-transfer rip=0x{transferTarget.Rip:X16} " +
$"rsp=0x{transferTarget.Rsp:X16} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} " +
$"fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16}");
}
return unchecked((ulong)transferFrame);
}
if (GuestThreadExecution.TryConsumeCurrentEntryExit(out var exitValue, out var exitReason))
{
if (TryCompleteGuestEntryToHostStub(argPackPtr, num, num7, importStubEntry.Nid, exitReason, exitValue))
{
cpuContext[CpuRegister.Rax] = exitValue;
}
else
{
@@ -357,9 +446,27 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
}
}
if (GuestThreadExecution.TryConsumeCurrentThreadBlock(out var blockReason) &&
if (GuestThreadExecution.TryConsumeCurrentThreadBlock(
out var blockReason,
out var blockContinuation,
out var hasBlockContinuation,
out var blockWakeKey,
out var blockResumeHandler,
out var blockWakeHandler,
out var blockDeadlineTimestamp) &&
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
{
if (hasBlockContinuation)
{
RegisterBlockedGuestThreadContinuation(
GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation,
blockWakeKey,
blockResumeHandler,
blockWakeHandler,
blockDeadlineTimestamp);
}
cpuContext[CpuRegister.Rax] = 0uL;
}
if (flag || flag2 || flag3)
@@ -380,6 +487,221 @@ public sealed partial class DirectExecutionBackend
}
}
private unsafe bool TryDispatchLeafImport(
CpuContext cpuContext,
ImportStubEntry importStubEntry,
nint argPackPtr,
long dispatchIndex,
out ulong result)
{
result = 0;
if (importStubEntry.Export is not { } export ||
(export.Target & cpuContext.TargetGeneration) == 0)
{
return false;
}
var arg0 = *(ulong*)argPackPtr;
var returnRip = *(ulong*)(argPackPtr + 96);
cpuContext.Rip = importStubEntry.Address;
cpuContext[CpuRegister.Rdi] = arg0;
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
cpuContext[CpuRegister.Rdx] = *(ulong*)(argPackPtr + 16);
cpuContext[CpuRegister.Rcx] = *(ulong*)(argPackPtr + 24);
cpuContext[CpuRegister.R8] = *(ulong*)(argPackPtr + 32);
cpuContext[CpuRegister.R9] = *(ulong*)(argPackPtr + 40);
cpuContext[CpuRegister.Rbx] = *(ulong*)(argPackPtr + 48);
cpuContext[CpuRegister.Rbp] = *(ulong*)(argPackPtr + 56);
cpuContext[CpuRegister.R12] = *(ulong*)(argPackPtr + 64);
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
if (_activeGuestThreadState is { } activeGuestThreadState)
{
Interlocked.Increment(ref activeGuestThreadState.ImportCount);
Volatile.Write(ref activeGuestThreadState.LastImportNid, importStubEntry.Nid);
Volatile.Write(ref activeGuestThreadState.LastReturnRip, returnRip);
}
if (dispatchIndex % 100000 == 0)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{dispatchIndex}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid})");
}
var previousImportCallFrame = GuestThreadExecution.EnterImportCallFrame(
returnRip,
(ulong)argPackPtr + 104uL,
ActiveGuestReturnSlotAddress);
int returnValue;
try
{
cpuContext.ClearRaxWriteFlag();
returnValue = export.Function(cpuContext);
if (!cpuContext.WasRaxWritten)
{
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
}
}
finally
{
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
}
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
var returnResult = (OrbisGen2Result)returnValue;
if (ShouldLogImportResult(importStubEntry.Nid, returnResult))
{
Console.Error.WriteLine(
$"[LOADER][WARN] Import#{dispatchIndex} result: {returnResult} ({importStubEntry.Nid}) " +
$"rdi=0x{arg0:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} " +
$"rdx=0x{cpuContext[CpuRegister.Rdx]:X16} rcx=0x{cpuContext[CpuRegister.Rcx]:X16} " +
$"r8=0x{cpuContext[CpuRegister.R8]:X16} r9=0x{cpuContext[CpuRegister.R9]:X16} " +
$"ret=0x{returnRip:X16}");
}
}
if (GuestThreadExecution.TryConsumeCurrentThreadBlock(
out var blockReason,
out var blockContinuation,
out var hasBlockContinuation,
out var blockWakeKey,
out var blockResumeHandler,
out var blockWakeHandler,
out var blockDeadlineTimestamp) &&
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
{
if (hasBlockContinuation)
{
RegisterBlockedGuestThreadContinuation(
GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation,
blockWakeKey,
blockResumeHandler,
blockWakeHandler,
blockDeadlineTimestamp);
}
cpuContext[CpuRegister.Rax] = 0uL;
}
result = cpuContext[CpuRegister.Rax];
return true;
}
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
{
var expectedFileProbeMiss =
result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND &&
IsExpectedFileProbeNotFoundNid(nid);
var expectedTimedWaitTimeout =
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
unchecked((int)result) == 60;
if (!expectedFileProbeMiss && !expectedTimedWaitTimeout)
{
return true;
}
var key = nid + "\0" + (int)result;
int count;
lock (_importResultLogSampleGate)
{
_importResultLogSamples.TryGetValue(key, out count);
count++;
_importResultLogSamples[key] = count;
}
return count <= 8 || count % 10000 == 0;
}
private static bool IsExpectedFileProbeNotFoundNid(string nid) =>
nid is
"eV9wAD2riIA" or // sceKernelStat
"1G3lF1Gg1k8" or // sceKernelOpen
"gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes
private bool IsLeafImport(string nid)
{
if (nid == "1jfXLRVzisc")
{
return !_logUsleep;
}
return nid is
"9UK1vLZQft4" or
"tn3VlD0hG60" or
"7H0iTOciTLo" or
"2Z+PpY6CaJg" or
"8aI7R7WaOlc" or
"a8uLzYY--tM" or
"Qs1xtplKo0U" or
"GuchCTefuZw" or
"N-FSPA4S3nI" or
"baQO9ez2gL4" or
"ULvXMDz56po" or
"mQ16-QdKv7k" or
"vWU-odnS+fU" or
"sSAUCCU1dv4" or
"C+IEj+BsAFM" or
"tZDDEo2tE5k" or
"GnxKOHEawhk" or
"H896Pt-yB4I" or
"sJXyWHjP-F8" or
"ASoW5WE-UPo" or
"rqwFKI4PAiM" or
"eE4Szl8sil8" or
"qvMUCyyaCSI" or
"27bAgiJmOh0" or // pthread_cond_timedwait
"j4ViWNHEgww" or // strlen
"5jNubw4vlAA" or // strnlen
"LHMrG7e8G78" or // wcslen
"WkkeywLJcgU" or // wcslen
"Ovb2dSJOAuE" or // strcmp
"aesyjrHVWy4" or // strncmp
"pNtJdE3x49E" or // wcscmp
"fV2xHER+bKE" or // wcscoll
"E8wCoUEbfzk" or // wcsncmp
"Q3VBxCXhUHs" or // memcpy
"+P6FRGH4LfA" or // memmove
"DfivPArhucg" or // memcmp
"ytQULN-nhL4" or // pthread_rwlock_init
"6ULAa0fq4jA" or // scePthreadRwlockInit
"1471ajPzxh0" or // pthread_rwlock_destroy
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
"iGjsr1WAtI0" or // pthread_rwlock_rdlock
"Ox9i0c7L5w0" or // scePthreadRwlockRdlock
"sIlRvQqsN2Y" or // pthread_rwlock_wrlock
"mqdNorrB+gI" or // scePthreadRwlockWrlock
"EgmLo6EWgso" or // pthread_rwlock_unlock
"+L98PIbGttk" or // scePthreadRwlockUnlock
"aI+OeCz8xrQ" or // scePthreadSelf
"EotR8a3ASf4" or // pthread_self
"eoht7mQOCmo" or // scePthreadGetspecific
"0-KXaS70xy4" or // pthread_getspecific
"+BzXYkqYeLE" or // scePthreadSetspecific
"WrOLvHU0yQM" or // pthread_setspecific
"vz+pg2zdopI" or // sceKernelGetEventUserData
"mJ7aghmgvfc" or // sceKernelGetEventId
"23CPPI1tyBY" or // sceKernelGetEventFilter
"kwGyyjohI50"; // sceKernelGetEventData
}
private long NextImportDispatchIndex()
{
if (!ReferenceEquals(_importCounterOwner, this) ||
_nextImportDispatchIndex >= _importDispatchBlockEnd)
{
var blockEnd = Interlocked.Add(ref _importDispatchCount, ImportDispatchBlockSize);
_importCounterOwner = this;
_nextImportDispatchIndex = blockEnd - ImportDispatchBlockSize + 1;
_importDispatchBlockEnd = blockEnd + 1;
}
return _nextImportDispatchIndex++;
}
private void TraceImportFrameChain(CpuContext context, long dispatchIndex)
{
var frame = context[CpuRegister.Rbp];
@@ -408,7 +730,7 @@ public sealed partial class DirectExecutionBackend
private unsafe bool TryForceGuestExitToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid)
{
ulong num = ActiveEntryReturnSentinelRip;
if (num < 65536)
if (num < 65536 || !TryPatchActiveGuestReturnSlot(num))
{
return false;
}
@@ -427,10 +749,10 @@ public sealed partial class DirectExecutionBackend
return true;
}
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, int status)
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, ulong value)
{
ulong hostExit = ActiveEntryReturnSentinelRip;
if (hostExit < 65536)
if (hostExit < 65536 || !TryPatchActiveGuestReturnSlot(hostExit))
{
return false;
}
@@ -443,14 +765,14 @@ public sealed partial class DirectExecutionBackend
return false;
}
Console.Error.WriteLine(
$"[LOADER][INFO] Guest entry exit at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={reason} status={status}");
$"[LOADER][INFO] Guest entry exit at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={reason} value=0x{value:X16}");
return true;
}
private unsafe bool TryYieldGuestThreadToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason)
{
ulong hostExit = ActiveEntryReturnSentinelRip;
if (hostExit < 65536)
if (hostExit < 65536 || !TryPatchActiveGuestReturnSlot(hostExit))
{
return false;
}
@@ -465,27 +787,47 @@ public sealed partial class DirectExecutionBackend
ActiveGuestThreadYieldRequested = true;
ActiveGuestThreadYieldReason = string.IsNullOrWhiteSpace(reason) ? nid : reason;
Console.Error.WriteLine(
$"[LOADER][INFO] Guest thread yield at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={ActiveGuestThreadYieldReason}");
if (_logGuestThreads)
{
Console.Error.WriteLine(
$"[LOADER][INFO] Guest thread yield at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={ActiveGuestThreadYieldReason}");
}
return true;
}
private bool TryPatchActiveGuestReturnSlot(ulong hostExit)
{
ulong returnSlotAddress = ActiveGuestReturnSlotAddress;
return returnSlotAddress != 0 &&
ActiveCpuContext is not null &&
ActiveCpuContext.TryWriteUInt64(returnSlotAddress, hostExit);
}
private bool ShouldForceGuestExitOnImportLoop(string nid, ulong returnRip, long dispatchIndex, ulong arg0, ulong arg1)
{
if (dispatchIndex < 1200)
{
return false;
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"), "1", StringComparison.Ordinal))
if (_disableImportLoopGuard || _importLoopGuardSeconds <= 0)
{
return false;
}
if (IsImportLoopGuardBoundary(nid))
{
ResetImportLoopPattern();
return false;
}
if (!_importNidHashCache.TryGetValue(nid, out var value))
{
value = StableHash64(nid);
_importNidHashCache[nid] = value;
}
RecordImportLoopSignature(value, returnRip, BuildImportLoopSignature(value, returnRip, arg0, arg1));
if ((dispatchIndex & 0x3F) != 0)
{
return false;
}
if (!HasRepeatingImportLoopPattern())
{
if (_importLoopPatternHits > 0)
@@ -503,14 +845,24 @@ public sealed partial class DirectExecutionBackend
_importLoopPatternStartTimestamp = Stopwatch.GetTimestamp();
}
_importLoopPatternHits++;
var guardSeconds = GetImportLoopGuardSeconds();
if (guardSeconds <= 0 || _importLoopPatternHits < 6)
if (_importLoopPatternHits < 6)
{
return false;
}
var elapsedTicks = Stopwatch.GetTimestamp() - _importLoopPatternStartTimestamp;
return elapsedTicks >= (long)(guardSeconds * Stopwatch.Frequency);
return elapsedTicks >= (long)(_importLoopGuardSeconds * Stopwatch.Frequency);
}
private static bool IsImportLoopGuardBoundary(string nid) =>
string.Equals(nid, "1jfXLRVzisc", StringComparison.Ordinal);
private void ResetImportLoopPattern()
{
_importLoopPatternHits = 0;
_importLoopPatternStartTimestamp = 0;
_importLoopSignatureCount = 0;
_importLoopSignatureWriteIndex = 0;
}
private static int GetImportLoopGuardSeconds()
@@ -782,8 +1134,7 @@ public sealed partial class DirectExecutionBackend
{
return result;
}
bool logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal);
if (logBootstrap)
if (_logBootstrap)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
@@ -923,66 +1274,6 @@ public sealed partial class DirectExecutionBackend
}
}
private void TryBypassStackChkFailTrap(long dispatchIndex, ulong returnRip)
{
var cpuContext = ActiveCpuContext;
if (cpuContext == null || returnRip < 32)
{
return;
}
try
{
byte[] array = new byte[19];
ulong num = returnRip - 23;
Marshal.Copy((nint)num, array, 0, array.Length);
if (array[0] != 117 || array[1] != 16 || array[2] != 72 || array[3] != 137 || array[4] != 216 || array[5] != 72 || array[6] != 131 || array[7] != 196 || array[9] != 91 || array[10] != 65 || array[11] != 92 || array[12] != 65 || array[13] != 94 || array[14] != 65 || array[15] != 95 || array[16] != 93 || array[17] != 195 || array[18] != 232)
{
return;
}
ulong value = returnRip - 21;
ulong address = cpuContext[CpuRegister.Rsp];
if (cpuContext.TryWriteUInt64(address, value))
{
if (_stackChkBypassSites.Add(num) && TryPatchStackChkFailBranch(num))
{
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: patched stack_chk_fail tail branch at 0x{num:X16} -> NOP NOP");
}
Console.Error.WriteLine($"[LOADER][WARNING] Import#{dispatchIndex}: redirected __stack_chk_fail return to epilogue 0x{value:X16}");
}
}
catch
{
}
}
private unsafe static bool TryPatchStackChkFailBranch(ulong branchAddress)
{
uint flNewProtect = default(uint);
if (!VirtualProtect((void*)branchAddress, 2u, 64u, &flNewProtect))
{
return false;
}
try
{
if (Marshal.ReadByte((nint)branchAddress) != 117)
{
return false;
}
Marshal.WriteByte((nint)branchAddress, 144);
Marshal.WriteByte((nint)(branchAddress + 1), 144);
FlushInstructionCache(GetCurrentProcess(), (void*)branchAddress, 2u);
return true;
}
catch
{
return false;
}
finally
{
VirtualProtect((void*)branchAddress, 2u, flNewProtect, &flNewProtect);
}
}
private unsafe void TryPatchEa020eLookupCall(long dispatchIndex, ulong returnRip)
{
if (_patchedEa020eLookupCall || returnRip != 0x0000000800EA01A6uL)
File diff suppressed because it is too large Load Diff
@@ -222,6 +222,56 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return actualAddress;
}
public bool TryAllocateAtOrAbove(
ulong desiredAddress,
ulong size,
bool executable,
ulong alignment,
out ulong actualAddress)
{
actualAddress = 0;
if (size == 0)
{
return false;
}
var alignedSize = AlignUp(size, PageSize);
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
var cursor = AlignUp(desiredAddress, effectiveAlignment);
for (var attempt = 0; attempt < 0x10000; attempt++)
{
if (cursor == 0 || ulong.MaxValue - cursor < alignedSize)
{
return false;
}
if (TryGetOverlappingRegionEnd(cursor, alignedSize, out var overlapEnd))
{
cursor = AlignUp(overlapEnd, effectiveAlignment);
continue;
}
try
{
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
if (actualAddress == cursor)
{
return true;
}
actualAddress = 0;
}
catch
{
}
cursor = AlignUp(cursor + effectiveAlignment, effectiveAlignment);
}
return false;
}
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
address = 0;
@@ -411,6 +461,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
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;
@@ -454,6 +514,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
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;
@@ -532,6 +602,30 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return null;
}
private bool TryGetOverlappingRegionEnd(ulong address, ulong size, out ulong overlapEnd)
{
overlapEnd = 0;
if (size == 0 || ulong.MaxValue - address < size - 1)
{
return false;
}
var end = address + size;
lock (_gate)
{
foreach (var region in _regions)
{
var regionEnd = region.VirtualAddress + region.Size;
if (address < regionEnd && region.VirtualAddress < end)
{
overlapEnd = Math.Max(overlapEnd, regionEnd);
}
}
}
return overlapEnd != 0;
}
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
{
offset = 0;
@@ -559,6 +653,44 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return protection is PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY;
}
private bool CanReadWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
CanAccessWithoutProtectionChange(address, size, region, write: false);
private bool CanWriteWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
CanAccessWithoutProtectionChange(address, size, region, write: true);
private bool CanAccessWithoutProtectionChange(ulong address, ulong size, MemoryRegion region, bool write)
{
var startPage = AlignDown(address, PageSize);
var endPage = AlignUp(address + size, PageSize);
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
{
if (_pageProtections.TryGetValue(pageAddress, out var flags))
{
if (write ? (flags & ProgramHeaderFlags.Write) == 0 : (flags & ProgramHeaderFlags.Read) == 0)
{
return false;
}
}
else if (write ? !IsWritableProtection(region.Protection) : !IsReadableProtection(region.Protection))
{
return false;
}
}
return true;
}
private static bool IsReadableProtection(uint protection)
{
return protection is PAGE_READONLY or PAGE_READWRITE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE;
}
private static bool IsWritableProtection(uint protection)
{
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
}
private static uint GetCommitProtection(MemoryRegion region)
{
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
+266 -12
View File
@@ -1,6 +1,8 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
namespace SharpEmu.HLE;
public readonly record struct GuestThreadStartRequest(
@@ -10,12 +12,27 @@ public readonly record struct GuestThreadStartRequest(
ulong AttributeAddress,
string Name);
public readonly record struct GuestThreadSnapshot(
ulong ThreadHandle,
string Name,
string State,
long ImportCount,
string? LastImportNid,
ulong LastReturnRip,
string? BlockReason);
public interface IGuestThreadScheduler
{
bool SupportsGuestContextTransfer { get; }
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
void Pump(CpuContext callerContext, string reason);
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
bool TryCallGuestFunction(
CpuContext callerContext,
ulong entryPoint,
@@ -36,11 +53,13 @@ public interface IGuestThreadScheduler
public readonly record struct GuestImportCallFrame(
bool IsValid,
ulong ReturnRip,
ulong ResumeRsp);
ulong ResumeRsp,
ulong ReturnSlotAddress);
public readonly record struct GuestCpuContinuation(
ulong Rip,
ulong Rsp,
ulong ReturnSlotAddress,
ulong Rflags,
ulong FsBase,
ulong GsBase,
@@ -63,18 +82,45 @@ public static class GuestThreadExecution
[ThreadStatic]
private static ulong _currentGuestThreadHandle;
[ThreadStatic]
private static ulong _currentFiberAddress;
[ThreadStatic]
private static string? _pendingBlockReason;
[ThreadStatic]
private static bool _pendingBlockContinuationValid;
[ThreadStatic]
private static GuestCpuContinuation _pendingBlockContinuation;
[ThreadStatic]
private static string? _pendingBlockWakeKey;
[ThreadStatic]
private static Func<int>? _pendingBlockResumeHandler;
[ThreadStatic]
private static Func<bool>? _pendingBlockWakeHandler;
[ThreadStatic]
private static long _pendingBlockDeadlineTimestamp;
[ThreadStatic]
private static bool _pendingEntryExit;
[ThreadStatic]
private static int _pendingEntryExitStatus;
private static ulong _pendingEntryExitValue;
[ThreadStatic]
private static string? _pendingEntryExitReason;
[ThreadStatic]
private static bool _pendingContextTransfer;
[ThreadStatic]
private static GuestCpuContinuation _pendingContextTransferTarget;
[ThreadStatic]
private static bool _hasCurrentImportCallFrame;
@@ -84,23 +130,37 @@ public static class GuestThreadExecution
[ThreadStatic]
private static ulong _currentImportResumeRsp;
[ThreadStatic]
private static ulong _currentImportReturnSlotAddress;
public static IGuestThreadScheduler? Scheduler { get; set; }
public static bool IsGuestThread => _currentGuestThreadHandle != 0;
public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle;
public static ulong CurrentFiberAddress => _currentFiberAddress;
public static ulong EnterGuestThread(ulong threadHandle)
{
var previous = _currentGuestThreadHandle;
_currentGuestThreadHandle = threadHandle;
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitStatus = 0;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
_pendingContextTransfer = false;
_pendingContextTransferTarget = default;
_hasCurrentImportCallFrame = false;
_currentImportReturnRip = 0;
_currentImportResumeRsp = 0;
_currentImportReturnSlotAddress = 0;
return previous;
}
@@ -108,15 +168,44 @@ public static class GuestThreadExecution
{
_currentGuestThreadHandle = previousThreadHandle;
_pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false;
_pendingEntryExitStatus = 0;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
_pendingContextTransfer = false;
_pendingContextTransferTarget = default;
_hasCurrentImportCallFrame = false;
_currentImportReturnRip = 0;
_currentImportResumeRsp = 0;
_currentImportReturnSlotAddress = 0;
}
public static bool RequestCurrentThreadBlock(string reason)
public static ulong EnterFiber(ulong fiberAddress)
{
var previous = _currentFiberAddress;
_currentFiberAddress = fiberAddress;
return previous;
}
public static void RestoreFiber(ulong previousFiberAddress)
{
_currentFiberAddress = previousFiberAddress;
}
public static bool RequestCurrentThreadBlock(string reason) => RequestCurrentThreadBlock(null, reason);
public static bool RequestCurrentThreadBlock(
CpuContext? context,
string reason,
string? wakeKey = null,
Func<int>? resumeHandler = null,
Func<bool>? wakeHandler = null,
long blockDeadlineTimestamp = 0)
{
if (!IsGuestThread)
{
@@ -124,31 +213,167 @@ public static class GuestThreadExecution
}
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
_pendingBlockResumeHandler = resumeHandler;
_pendingBlockWakeHandler = wakeHandler;
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
{
_pendingBlockContinuation = continuation;
_pendingBlockContinuationValid = true;
}
else
{
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
}
return true;
}
public static bool TryConsumeCurrentThreadBlock(out string reason)
{
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out _,
out _,
out _,
out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler)
{
return TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out wakeKey,
out resumeHandler,
out wakeHandler,
out _);
}
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler,
out long blockDeadlineTimestamp)
{
reason = _pendingBlockReason ?? string.Empty;
if (string.IsNullOrEmpty(reason))
{
continuation = default;
hasContinuation = false;
wakeKey = string.Empty;
resumeHandler = null;
wakeHandler = null;
blockDeadlineTimestamp = 0;
return false;
}
continuation = _pendingBlockContinuation;
hasContinuation = _pendingBlockContinuationValid;
wakeKey = _pendingBlockWakeKey ?? reason;
resumeHandler = _pendingBlockResumeHandler;
wakeHandler = _pendingBlockWakeHandler;
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
_pendingBlockReason = null;
_pendingBlockContinuation = default;
_pendingBlockContinuationValid = false;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
return true;
}
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
{
if (timeout <= TimeSpan.Zero)
{
return Stopwatch.GetTimestamp();
}
var ticks = timeout.TotalSeconds >= long.MaxValue / (double)Stopwatch.Frequency
? long.MaxValue
: (long)Math.Ceiling(timeout.TotalSeconds * Stopwatch.Frequency);
var now = Stopwatch.GetTimestamp();
if (long.MaxValue - now <= ticks)
{
return long.MaxValue;
}
return now + Math.Max(1, ticks);
}
private static bool TryCaptureCurrentBlockContinuation(CpuContext context, out GuestCpuContinuation continuation)
{
if (!TryGetCurrentImportCallFrame(out var frame) ||
frame.ReturnRip < 65536 ||
frame.ResumeRsp == 0 ||
frame.ReturnSlotAddress == 0)
{
continuation = default;
return false;
}
continuation = new GuestCpuContinuation(
frame.ReturnRip,
frame.ResumeRsp,
frame.ReturnSlotAddress,
context.Rflags,
context.FsBase,
context.GsBase,
0,
context[CpuRegister.Rcx],
context[CpuRegister.Rdx],
context[CpuRegister.Rbx],
context[CpuRegister.Rbp],
context[CpuRegister.Rsi],
context[CpuRegister.Rdi],
context[CpuRegister.R8],
context[CpuRegister.R9],
context[CpuRegister.R12],
context[CpuRegister.R13],
context[CpuRegister.R14],
context[CpuRegister.R15]);
return true;
}
public static void RequestCurrentEntryExit(string reason, int status)
{
RequestCurrentEntryExit(reason, unchecked((ulong)(long)status));
}
public static void RequestCurrentEntryExit(string reason, ulong value)
{
_pendingEntryExit = true;
_pendingEntryExitStatus = status;
_pendingEntryExitValue = value;
_pendingEntryExitReason = string.IsNullOrWhiteSpace(reason) ? "guest_entry_exit" : reason;
}
public static bool TryConsumeCurrentEntryExit(out int status, out string reason)
public static bool TryConsumeCurrentEntryExit(out ulong value, out string reason)
{
status = _pendingEntryExitStatus;
value = _pendingEntryExitValue;
reason = _pendingEntryExitReason ?? string.Empty;
if (!_pendingEntryExit)
{
@@ -156,20 +381,44 @@ public static class GuestThreadExecution
}
_pendingEntryExit = false;
_pendingEntryExitStatus = 0;
_pendingEntryExitValue = 0;
_pendingEntryExitReason = null;
return true;
}
public static GuestImportCallFrame EnterImportCallFrame(ulong returnRip, ulong resumeRsp)
public static void RequestCurrentContextTransfer(GuestCpuContinuation target)
{
_pendingContextTransferTarget = target;
_pendingContextTransfer = true;
}
public static bool TryConsumeCurrentContextTransfer(out GuestCpuContinuation target)
{
target = _pendingContextTransferTarget;
if (!_pendingContextTransfer)
{
return false;
}
_pendingContextTransfer = false;
_pendingContextTransferTarget = default;
return true;
}
public static GuestImportCallFrame EnterImportCallFrame(
ulong returnRip,
ulong resumeRsp,
ulong returnSlotAddress)
{
var previous = new GuestImportCallFrame(
_hasCurrentImportCallFrame,
_currentImportReturnRip,
_currentImportResumeRsp);
_currentImportResumeRsp,
_currentImportReturnSlotAddress);
_hasCurrentImportCallFrame = true;
_currentImportReturnRip = returnRip;
_currentImportResumeRsp = resumeRsp;
_currentImportReturnSlotAddress = returnSlotAddress;
return previous;
}
@@ -178,6 +427,7 @@ public static class GuestThreadExecution
_hasCurrentImportCallFrame = previous.IsValid;
_currentImportReturnRip = previous.ReturnRip;
_currentImportResumeRsp = previous.ResumeRsp;
_currentImportReturnSlotAddress = previous.ReturnSlotAddress;
}
public static bool TryGetCurrentImportCallFrame(out GuestImportCallFrame frame)
@@ -188,7 +438,11 @@ public static class GuestThreadExecution
return false;
}
frame = new GuestImportCallFrame(true, _currentImportReturnRip, _currentImportResumeRsp);
frame = new GuestImportCallFrame(
true,
_currentImportReturnRip,
_currentImportResumeRsp,
_currentImportReturnSlotAddress);
return true;
}
}
+198 -8
View File
@@ -30,10 +30,13 @@ public static class AgcExports
private const uint RUcRegsIndirect = 0x13;
private const uint RAcquireMem = 0x14;
private const uint RFlip = 0x17;
private const uint RReleaseMem = 0x18;
private const uint SpiShaderPgmLoPs = 0x8;
private const uint SpiShaderPgmHiPs = 0x9;
private const uint SpiShaderPgmLoEs = 0xC8;
private const uint SpiShaderPgmHiEs = 0xC9;
private const uint SpiShaderPgmLoLs = 0x148;
private const uint SpiShaderPgmHiLs = 0x149;
private const uint SpiPsInputEna = 0x1B3;
private const uint SpiPsInputAddr = 0x1B4;
private const uint ComputePgmLo = 0x20C;
@@ -45,8 +48,11 @@ public static class AgcExports
private const uint Gen5TextureType2D = 9;
private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
private const ulong VideoOutPixelFormatB8G8R8A8Unorm = 0x8100000000000000;
private const ulong VideoOutPixelFormatR8G8B8A8Unorm = 0x8100000022000000;
private const uint RegisterDefaultsVersion7 = 7;
private const uint RegisterDefaultsVersion8 = 8;
private const uint RegisterDefaultsVersion10 = 10;
private const int RegisterDefaultsSize = 0x40;
private const int RegisterDefaultBlockSize = 16 * 8;
@@ -345,7 +351,7 @@ public static class AgcExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!TryReadByte(ctx, geometryShaderAddress + ShaderTypeOffset, out var shaderType) || shaderType != 2 ||
if (!TryReadByte(ctx, geometryShaderAddress + ShaderTypeOffset, out var shaderType) || !IsEsGeometryShaderType(shaderType) ||
!TryReadUInt64(ctx, geometryShaderAddress + ShaderSpecialsOffset, out var specialsAddress) ||
specialsAddress == 0)
{
@@ -362,7 +368,7 @@ public static class AgcExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAgc($"agc.create_prim_state cx=0x{cxRegistersAddress:X16} uc=0x{ucRegistersAddress:X16} gs=0x{geometryShaderAddress:X16} prim=0x{primitiveType:X8}");
TraceAgc($"agc.create_prim_state cx=0x{cxRegistersAddress:X16} uc=0x{ucRegistersAddress:X16} gs=0x{geometryShaderAddress:X16} type={shaderType} prim=0x{primitiveType:X8}");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -495,6 +501,64 @@ public static class AgcExports
return ReturnPointer(ctx, commandAddress);
}
[SysAbiExport(
Nid = "wr23dPKyWc0",
ExportName = "sceAgcCbReleaseMem",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int CbReleaseMem(CpuContext ctx)
{
var commandBufferAddress = ctx[CpuRegister.Rdi];
var action = (uint)(ctx[CpuRegister.Rsi] & 0xFF);
var gcrControl = (uint)(ctx[CpuRegister.Rdx] & 0xFFFF);
var destination = (uint)(ctx[CpuRegister.Rcx] & 0xFF);
var cachePolicy = (uint)(ctx[CpuRegister.R8] & 0xFF);
var destinationAddress = ctx[CpuRegister.R9];
var stackAddress = ctx[CpuRegister.Rsp];
if (!TryReadUInt64(ctx, stackAddress + 8, out var dataSelectionRaw) ||
!TryReadUInt64(ctx, stackAddress + 16, out var data) ||
!TryReadUInt64(ctx, stackAddress + 24, out var gdsOffsetRaw) ||
!TryReadUInt64(ctx, stackAddress + 32, out var gdsSizeRaw) ||
!TryReadUInt64(ctx, stackAddress + 40, out var interruptRaw) ||
!TryReadUInt64(ctx, stackAddress + 48, out var interruptContextIdRaw))
{
return ReturnPointer(ctx, 0);
}
var dataSelection = (uint)(dataSelectionRaw & 0xFF);
var gdsOffset = (uint)(gdsOffsetRaw & 0xFFFF);
var gdsSize = (uint)(gdsSizeRaw & 0xFFFF);
var interrupt = (uint)(interruptRaw & 0xFF);
var interruptContextId = (uint)interruptContextIdRaw;
if (commandBufferAddress == 0 ||
destination != 1 ||
dataSelection is not (2 or 3) ||
gdsOffset != 0 ||
gdsSize != 1 ||
interrupt != 0 ||
interruptContextId != 0)
{
return ReturnPointer(ctx, 0);
}
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) ||
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RReleaseMem)) ||
!TryWriteUInt32(ctx, commandAddress + 4, action | (cachePolicy << 8)) ||
!TryWriteUInt32(ctx, commandAddress + 8, gcrControl | (dataSelection << 16)) ||
!TryWriteUInt32(ctx, commandAddress + 12, (uint)destinationAddress) ||
!TryWriteUInt32(ctx, commandAddress + 16, (uint)(destinationAddress >> 32)) ||
!TryWriteUInt32(ctx, commandAddress + 20, (uint)data) ||
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(data >> 32)))
{
return ReturnPointer(ctx, 0);
}
TraceAgc(
$"agc.cb_release_mem buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " +
$"action=0x{action:X2} gcr=0x{gcrControl:X4} dst=0x{destinationAddress:X16} data_sel={dataSelection} data=0x{data:X16}");
return ReturnPointer(ctx, commandAddress);
}
[SysAbiExport(
Nid = "TRO721eVt4g",
ExportName = "sceAgcDcbResetQueue",
@@ -823,6 +887,50 @@ public static class AgcExports
return ReturnPointer(ctx, commandAddress);
}
[SysAbiExport(
Nid = "w2rJhmD+dsE",
ExportName = "sceAgcDriverAddEqEvent",
Target = Generation.Gen5,
LibraryName = "libSceAgcDriver")]
public static int DriverAddEqEvent(CpuContext ctx)
{
var equeue = ctx[CpuRegister.Rdi];
var eventId = ctx[CpuRegister.Rsi];
var userData = ctx[CpuRegister.Rdx];
if (!KernelEventQueueCompatExports.RegisterEvent(
equeue,
eventId,
KernelEventQueueCompatExports.KernelEventFilterGraphics,
userData))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
TraceAgc($"agc.driver_add_eq_event eq=0x{equeue:X16} id=0x{eventId:X16} udata=0x{userData:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "DL2RXaXOy88",
ExportName = "sceAgcDriverDeleteEqEvent",
Target = Generation.Gen5,
LibraryName = "libSceAgcDriver")]
public static int DriverDeleteEqEvent(CpuContext ctx)
{
var equeue = ctx[CpuRegister.Rdi];
var eventId = ctx[CpuRegister.Rsi];
if (!KernelEventQueueCompatExports.DeleteRegisteredEvent(
equeue,
eventId,
KernelEventQueueCompatExports.KernelEventFilterGraphics))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
TraceAgc($"agc.driver_delete_eq_event eq=0x{equeue:X16} id=0x{eventId:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "UglJIZjGssM",
ExportName = "sceAgcDriverSubmitDcb",
@@ -869,6 +977,20 @@ public static class AgcExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "qj7QZpgr9Uw",
ExportName = "sceAgcUnknownQj7QZpgr9Uw",
Target = Generation.Gen5,
LibraryName = "libSceAgc")]
public static int UnknownQj7QZpgr9Uw(CpuContext ctx)
{
TraceAgc(
$"agc.unknown_qj7 rdi=0x{ctx[CpuRegister.Rdi]:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} " +
$"rdx=0x{ctx[CpuRegister.Rdx]:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16}");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void ParseSubmittedDcb(CpuContext ctx, ulong commandAddress, uint dwordCount, bool tracePackets)
{
if (commandAddress == 0 || dwordCount == 0 || dwordCount > 1_000_000)
@@ -910,6 +1032,26 @@ public static class AgcExports
ApplySubmittedRegisters(ctx, state, currentAddress, length, op, register);
if (op == ItEventWrite &&
length >= 2 &&
TryReadUInt32(ctx, currentAddress + sizeof(uint), out var eventTypeRaw))
{
var eventType = eventTypeRaw & 0x3Fu;
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEvents(
eventType,
KernelEventQueueCompatExports.KernelEventFilterGraphics,
eventType);
if (tracePackets)
{
TraceAgc($"agc.dcb.event type=0x{eventType:X2} queues={triggered}");
}
}
if (op == ItNop && register == RReleaseMem && length >= 7)
{
ApplySubmittedReleaseMem(ctx, currentAddress, tracePackets);
}
if (op == ItDrawIndexOffset2 &&
length >= 5 &&
TryReadUInt32(ctx, currentAddress + 4, out var indexCount) &&
@@ -970,6 +1112,38 @@ public static class AgcExports
}
}
private static void ApplySubmittedReleaseMem(
CpuContext ctx,
ulong packetAddress,
bool tracePacket)
{
if (!TryReadUInt32(ctx, packetAddress + 8, out var control) ||
!TryReadUInt32(ctx, packetAddress + 12, out var destinationLo) ||
!TryReadUInt32(ctx, packetAddress + 16, out var destinationHi) ||
!TryReadUInt32(ctx, packetAddress + 20, out var dataLo) ||
!TryReadUInt32(ctx, packetAddress + 24, out var dataHi))
{
return;
}
var dataSelection = (control >> 16) & 0xFFu;
var destinationAddress = ((ulong)destinationHi << 32) | destinationLo;
var data = ((ulong)dataHi << 32) | dataLo;
var wroteData = dataSelection switch
{
2 => TryWriteUInt32(ctx, destinationAddress, dataLo),
3 => ctx.TryWriteUInt64(destinationAddress, data),
_ => false,
};
if (tracePacket)
{
TraceAgc(
$"agc.dcb.release_mem dst=0x{destinationAddress:X16} data_sel={dataSelection} " +
$"data=0x{data:X16} wrote={wroteData}");
}
}
private static void ApplySubmittedRegisters(
CpuContext ctx,
SubmittedDcbState state,
@@ -1150,7 +1324,11 @@ public static class AgcExports
destination.Width > 8192 ||
destination.Height > 8192 ||
destination.TilingMode != 0 ||
destination.PixelFormat is not (VideoOutPixelFormatA8R8G8B8Srgb or VideoOutPixelFormatA8B8G8R8Srgb))
destination.PixelFormat is not (
VideoOutPixelFormatA8R8G8B8Srgb or
VideoOutPixelFormatA8B8G8R8Srgb or
VideoOutPixelFormatB8G8R8A8Unorm or
VideoOutPixelFormatR8G8B8A8Unorm))
{
return false;
}
@@ -1187,7 +1365,9 @@ public static class AgcExports
}
var destinationRow = new byte[checked((int)destinationPitch * 4)];
var rgbaDestination = destination.PixelFormat == VideoOutPixelFormatA8B8G8R8Srgb;
var rgbaDestination = destination.PixelFormat is
VideoOutPixelFormatA8B8G8R8Srgb or
VideoOutPixelFormatR8G8B8A8Unorm;
for (uint y = 0; y < destination.Height; y++)
{
var sourceY = (uint)(((ulong)y * source.Height) / destination.Height);
@@ -1322,14 +1502,16 @@ public static class AgcExports
{
0 => ComputePgmLo,
1 => SpiShaderPgmLoPs,
2 => SpiShaderPgmLoEs,
2 or 6 => SpiShaderPgmLoEs,
7 => SpiShaderPgmLoLs,
_ => 0u,
};
var expectedHi = shaderType switch
{
0 => ComputePgmHi,
1 => SpiShaderPgmHiPs,
2 => SpiShaderPgmHiEs,
2 or 6 => SpiShaderPgmHiEs,
7 => SpiShaderPgmHiLs,
_ => 0u,
};
if (expectedLo == 0 || loRegister != expectedLo || hiRegister != expectedHi)
@@ -1344,6 +1526,9 @@ public static class AgcExports
TryWriteUInt32(ctx, shRegistersAddress + 8 + sizeof(uint), hiValue);
}
private static bool IsEsGeometryShaderType(byte shaderType) =>
shaderType is 2 or 6;
private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace)
{
var commandAddress = ctx[CpuRegister.Rdi];
@@ -1486,7 +1671,10 @@ public static class AgcExports
private static bool IsSupportedRegisterDefaultsVersion(uint version)
{
return version is RegisterDefaultsVersion7 or RegisterDefaultsVersion8;
return version is
RegisterDefaultsVersion7 or
RegisterDefaultsVersion8 or
RegisterDefaultsVersion10;
}
private static bool TryGetRegisterDefaultsAllocation(
@@ -1689,7 +1877,9 @@ public static class AgcExports
private static void TraceCreateShader(ulong destinationAddress, ulong headerAddress, ulong codeAddress, string detail)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal))
var isOk = string.Equals(detail, "ok", StringComparison.Ordinal);
if (isOk &&
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal))
{
return;
}
+189 -16
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Buffers.Binary;
using System.Collections.Concurrent;
@@ -17,9 +18,13 @@ public static class AmprExports
private const ulong CommandBufferAux1Offset = 0x20;
private const ulong ReadFileRecordSize = 0x30;
private const ulong KernelEventQueueRecordSize = 0x30;
private const ulong WriteAddressRecordSize = 0x20;
private const uint ReadFileRecordType = 1;
private const uint KernelEventQueueRecordType = 2;
private const uint WriteAddressRecordType = 3;
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
private static readonly bool _traceAmpr =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
private sealed class CommandBufferState
{
@@ -284,6 +289,18 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "C+IEj+BsAFM",
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeWriteAddressOnCompletion(CpuContext ctx)
{
TraceAmpr(ctx, "measure_write_address_complete", 0, WriteAddressRecordSize, 0);
ctx[CpuRegister.Rax] = WriteAddressRecordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "tZDDEo2tE5k",
ExportName = "sceAmprCommandBufferGetSize",
@@ -340,28 +357,116 @@ public static class AmprExports
var commandBuffer = ctx[CpuRegister.Rdi];
var equeue = ctx[CpuRegister.Rsi];
var ident = ctx[CpuRegister.Rdx];
var filter = ctx[CpuRegister.Rcx];
var completionToken = ctx[CpuRegister.Rcx];
var userData = ctx[CpuRegister.R8];
var data = ctx[CpuRegister.R9];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var extra = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out extra);
if (!AppendKernelEventQueueRecord(ctx, commandBuffer, equeue, ident, filter, userData, data, extra))
if (!AppendKernelEventQueueRecord(
ctx,
commandBuffer,
equeue,
ident,
completionToken,
userData))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "write_equeue", commandBuffer, equeue, ident);
TraceAmpr(ctx, "write_equeue", commandBuffer, ident, completionToken);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "sJXyWHjP-F8",
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferWriteAddressOnCompletion(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var address = ctx[CpuRegister.Rsi];
var value = ctx[CpuRegister.Rdx];
if (commandBuffer == 0 || address == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!AppendWriteAddressRecord(ctx, commandBuffer, address, value))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "write_address_complete", commandBuffer, address, value);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static int CompleteCommandBuffer(CpuContext ctx, ulong commandBuffer)
{
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out var buffer, out _, out var state) || state is null)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ulong writeOffset;
lock (state)
{
writeOffset = state.WriteOffset;
}
var offset = 0UL;
while (offset < writeOffset)
{
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
switch (recordType)
{
case ReadFileRecordType:
offset += ReadFileRecordSize;
break;
case KernelEventQueueRecordType:
if (!CompleteKernelEventQueueRecord(ctx, buffer + offset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
offset += KernelEventQueueRecordSize;
break;
case WriteAddressRecordType:
if (!CompleteWriteAddressRecord(ctx, buffer + offset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
offset += WriteAddressRecordSize;
break;
default:
TraceAmpr(ctx, "complete_unknown", commandBuffer, recordType, offset);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
TraceAmpr(ctx, "complete", commandBuffer, buffer, writeOffset);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool InitializeCommandBuffer(
CpuContext ctx,
ulong commandBuffer,
@@ -558,20 +663,28 @@ public static class AmprExports
ulong commandBuffer,
ulong equeue,
ulong ident,
ulong filter,
ulong userData,
ulong data,
ulong extra)
ulong completionToken,
ulong userData)
{
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
record.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType);
BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], unchecked((uint)filter));
BinaryPrimitives.WriteInt16LittleEndian(record[0x04..], KernelEventQueueCompatExports.KernelEventFilterAmpr);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], equeue);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], data);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x28..], extra);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], completionToken);
return AppendCommandBufferRecord(ctx, commandBuffer, record);
}
private static bool AppendWriteAddressRecord(CpuContext ctx, ulong commandBuffer, ulong address, ulong value)
{
Span<byte> record = stackalloc byte[(int)WriteAddressRecordSize];
record.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], WriteAddressRecordType);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], address);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], value);
return AppendCommandBufferRecord(ctx, commandBuffer, record);
}
@@ -604,9 +717,69 @@ public static class AmprExports
return true;
}
private static bool CompleteKernelEventQueueRecord(CpuContext ctx, ulong recordAddress)
{
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
if (!ctx.Memory.TryRead(recordAddress, record))
{
return false;
}
var filter = unchecked((short)BinaryPrimitives.ReadUInt32LittleEndian(record[0x04..]));
var equeue = BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]);
var ident = BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]);
var userData = BinaryPrimitives.ReadUInt64LittleEndian(record[0x18..]);
var data = BinaryPrimitives.ReadUInt64LittleEndian(record[0x20..]);
var extra = BinaryPrimitives.ReadUInt64LittleEndian(record[0x28..]);
var queuedEvent = new KernelEventQueueCompatExports.KernelQueuedEvent(
ident,
filter,
0x20,
unchecked((uint)extra),
data,
userData);
_ = KernelEventQueueCompatExports.EnqueueEvent(equeue, queuedEvent);
TraceAmpr(ctx, "complete_equeue", equeue, ident, data);
return true;
}
private static bool CompleteWriteAddressRecord(CpuContext ctx, ulong recordAddress)
{
Span<byte> record = stackalloc byte[(int)WriteAddressRecordSize];
if (!ctx.Memory.TryRead(recordAddress, record))
{
return false;
}
var address = BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]);
var value = BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]);
if (!ctx.TryWriteUInt64(address, value))
{
return false;
}
TraceAmpr(ctx, "complete_write_address", address, value, 0);
return true;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
if (!_traceAmpr)
{
return;
}
@@ -628,7 +801,7 @@ public static class AmprExports
string? hostPath,
int result)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
if (!_traceAmpr)
{
return;
}
@@ -4,6 +4,7 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
using System.Text.Json;
namespace SharpEmu.Libs.AppContent;
@@ -11,6 +12,8 @@ public static class AppContentExports
{
private const ulong BootParamAttrOffset = 4;
private const string Temp0MountPoint = "/temp0";
private const uint AppParamSkuFlag = 0;
private const int AppParamSkuFlagFull = 3;
[SysAbiExport(
Nid = "R9lA82OraNs",
@@ -59,6 +62,41 @@ public static class AppContentExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "99b82IKXpH4",
ExportName = "sceAppContentAppParamGetInt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentAppParamGetInt(CpuContext ctx)
{
var paramId = (uint)ctx[CpuRegister.Rdi];
var valueAddress = ctx[CpuRegister.Rsi];
if (valueAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
int value;
if (paramId == AppParamSkuFlag)
{
value = AppParamSkuFlagFull;
}
else if (!TryReadUserDefinedParam(paramId, out value))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAppContent($"app_param_get_int id={paramId} value={value}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "buYbeLOGWmA",
ExportName = "sceAppContentTemporaryDataMount2",
@@ -83,6 +121,69 @@ public static class AppContentExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryReadUserDefinedParam(uint paramId, out int value)
{
value = 0;
if (paramId is < 1 or > 4)
{
return false;
}
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
if (string.IsNullOrWhiteSpace(app0Root))
{
return true;
}
var paramJsonPath = Path.Combine(app0Root, "sce_sys", "param.json");
if (!File.Exists(paramJsonPath))
{
return true;
}
try
{
using var stream = File.OpenRead(paramJsonPath);
using var document = JsonDocument.Parse(stream);
var propertyName = $"userDefinedParam{paramId}";
if (document.RootElement.TryGetProperty(propertyName, out var element) &&
element.TryGetInt32(out var parsedValue))
{
value = parsedValue;
}
return true;
}
catch (IOException)
{
return true;
}
catch (UnauthorizedAccessException)
{
return true;
}
catch (JsonException)
{
return true;
}
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
private static void TraceAppContent(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] app_content.{message}");
}
private static string ResolveTemp0Root()
{
const string temp0VariableName = "SHARPEMU_TEMP0_DIR";
+231
View File
@@ -0,0 +1,231 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.Audio;
public static class AudioOut2Exports
{
private const int AudioOut2ContextParamSize = 0x80;
private const int AudioOut2ContextMemorySize = 0x10000;
private const int AudioOut2ContextMemoryAlignment = 0x10000;
private static long _nextContextHandle = 1;
private static long _nextUserHandle = 1;
private static int _nextPortId;
[SysAbiExport(
Nid = "g2tViFIohHE",
ExportName = "sceAudioOut2Initialize",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2Initialize(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "t5YrizufpQc",
ExportName = "sceAudioOut2ContextResetParam",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextResetParam(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
param.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(param[0x00..], AudioOut2ContextParamSize);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x04..], 2);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x08..], 48000);
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
return ctx.Memory.TryWrite(paramAddress, param)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "pDmme7Bgm6E",
ExportName = "sceAudioOut2ContextQueryMemory",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> memoryInfo = stackalloc byte[0x20];
memoryInfo.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "0x6o1VVAYSY",
ExportName = "sceAudioOut2ContextCreate",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextCreate(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var memoryAddress = ctx[CpuRegister.Rsi];
var memorySize = ctx[CpuRegister.Rdx];
var outContextAddress = ctx[CpuRegister.Rcx];
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
return TryWriteUInt64(ctx, outContextAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "on6ZH7Abo10",
ExportName = "sceAudioOut2ContextDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "JK2wamZPzwM",
ExportName = "sceAudioOut2PortCreate",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortCreate(CpuContext ctx)
{
var type = unchecked((int)ctx[CpuRegister.Rdi]);
var paramAddress = ctx[CpuRegister.Rsi];
var outPortAddress = ctx[CpuRegister.Rdx];
var contextAddress = ctx[CpuRegister.Rcx];
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
return TryWriteUInt64(ctx, outPortAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "gatEUKG+Ea4",
ExportName = "sceAudioOut2PortGetState",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortGetState(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
var stateAddress = ctx[CpuRegister.Rsi];
if (handle == 0 || stateAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var type = (int)((handle >> 16) & 0xFF);
Span<byte> state = stackalloc byte[0x20];
state.Clear();
var output = type == 2 ? 0x40 : 0x01;
var channels = type == 2 ? 1 : 2;
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], unchecked((ushort)output));
state[0x02] = unchecked((byte)channels);
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
return ctx.Memory.TryWrite(stateAddress, state)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "DImz2Ft9E2g",
ExportName = "sceAudioOut2GetSpeakerInfo",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2GetSpeakerInfo(CpuContext ctx)
{
var infoAddress = ctx[CpuRegister.Rdi];
if (infoAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> info = stackalloc byte[0x40];
info.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 1);
BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 2);
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
return ctx.Memory.TryWrite(infoAddress, info)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "cd+Rtw+D1x8",
ExportName = "sceAudioOut2PortDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "IaZXJ9M79uo",
ExportName = "sceAudioOut2UserDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "xywYcRB7nbQ",
ExportName = "sceAudioOut2UserCreate",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2UserCreate(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
return TryWriteUInt64(ctx, outUserAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
+216 -148
View File
@@ -48,15 +48,10 @@ public static class FiberExports
[ThreadStatic]
private static ulong _currentFiberAddress;
[ThreadStatic]
private static bool _fiberReturnRequested;
[ThreadStatic]
private static ulong _fiberReturnArgument;
private static readonly object _fiberGate = new();
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
private static readonly ConcurrentDictionary<ulong, FiberReturnTarget> _returnTargets = new();
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
private static readonly ConcurrentDictionary<ulong, FiberRunSession> _runSessions = new();
[SysAbiExport(
Nid = "hVYD7Ou2pCQ",
@@ -146,6 +141,7 @@ public static class FiberExports
}
_continuations.TryRemove(fiber, out _);
_returnTargets.TryRemove(fiber, out _);
_stackRanges.TryRemove(fiber, out _);
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
return SetReturn(ctx, 0);
@@ -230,44 +226,76 @@ public static class FiberExports
LibraryName = "libSceFiber")]
public static int FiberReturnToThread(CpuContext ctx)
{
var fiberAddress = _currentFiberAddress;
var inferredFiber = false;
if (fiberAddress == 0 && TryFindFiberByStack(ctx, out fiberAddress))
{
inferredFiber = true;
}
var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0)
{
return SetReturn(ctx, FiberErrorPermission);
}
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{
return SetReturn(ctx, FiberErrorPermission);
}
var returnArgument = ctx[CpuRegister.Rdi];
var argOnRunAddress = ctx[CpuRegister.Rsi];
if (argOnRunAddress != 0 && !TryWriteUInt64(ctx, argOnRunAddress, 0))
{
return SetReturn(ctx, FiberErrorInvalid);
}
_fiberReturnRequested = true;
_fiberReturnArgument = ctx[CpuRegister.Rdi];
if (_runSessions.TryGetValue(fiberAddress, out var session))
{
session.SetReturn(ctx[CpuRegister.Rdi]);
}
if (GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
GuestCpuContinuation transferTarget;
ulong previousFiber;
lock (_fiberGate)
{
_continuations[fiberAddress] = new FiberContinuation(
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp),
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
argOnRunAddress);
TraceFiber($"yield{(inferredFiber ? "-inferred" : string.Empty)} fiber=0x{fiberAddress:X16} resume=0x{frame.ReturnRip:X16} rsp=0x{frame.ResumeRsp:X16} arg_out=0x{argOnRunAddress:X16}");
}
else
{
TraceFiber($"yield-no-frame{(inferredFiber ? "-inferred" : string.Empty)} fiber=0x{fiberAddress:X16} arg_out=0x{argOnRunAddress:X16}");
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
{
_continuations.TryRemove(fiberAddress, out _);
return SetReturn(ctx, FiberErrorPermission);
}
previousFiber = returnTarget.PreviousFiber;
if (previousFiber != 0)
{
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun))
{
_continuations.TryRemove(fiberAddress, out _);
return SetReturn(ctx, FiberErrorState);
}
transferTarget = previousContinuation.Context with { Rax = 0 };
}
else
{
if (!returnTarget.ThreadContinuation.HasValue ||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
{
_continuations.TryRemove(fiberAddress, out _);
return SetReturn(ctx, FiberErrorState);
}
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
}
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
{
return SetReturn(ctx, FiberErrorInvalid);
}
}
GuestThreadExecution.RequestCurrentEntryExit("sceFiberReturnToThread", 0);
_currentFiberAddress = previousFiber;
_ = GuestThreadExecution.EnterFiber(previousFiber);
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
TraceFiber(
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
return SetReturn(ctx, 0);
}
@@ -284,12 +312,13 @@ public static class FiberExports
return SetReturn(ctx, FiberErrorNull);
}
if (_currentFiberAddress == 0)
var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0)
{
return SetReturn(ctx, FiberErrorPermission);
}
return TryWriteUInt64(ctx, outAddress, _currentFiberAddress)
return TryWriteUInt64(ctx, outAddress, fiberAddress)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
@@ -407,7 +436,7 @@ public static class FiberExports
return SetReturn(ctx, FiberErrorNull);
}
if (_currentFiberAddress == 0)
if (ResolveCurrentFiberAddress(ctx) == 0)
{
return SetReturn(ctx, FiberErrorPermission);
}
@@ -535,119 +564,161 @@ public static class FiberExports
}
}
if (fields.State != FiberStateIdle)
var previousFiber = ResolveCurrentFiberAddress(ctx);
if ((isSwitch && previousFiber == 0) ||
(!isSwitch && previousFiber != 0))
{
return SetReturn(ctx, FiberErrorPermission);
}
if (previousFiber == fiber)
{
return SetReturn(ctx, FiberErrorState);
}
var previousFiber = _currentFiberAddress;
var switchingFromFiber = isSwitch && previousFiber != 0 && previousFiber != fiber;
if (isSwitch && previousFiber == 0)
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{
return SetReturn(ctx, FiberErrorPermission);
}
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
GuestCpuContinuation transferTarget;
var resumed = false;
lock (_fiberGate)
{
return SetReturn(ctx, FiberErrorPermission);
}
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (switchingFromFiber && !TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
{
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle);
return SetReturn(ctx, FiberErrorInvalid);
}
var previousReturnRequested = _fiberReturnRequested;
var previousReturnArgument = _fiberReturnArgument;
var session = new FiberRunSession();
_runSessions[fiber] = session;
_currentFiberAddress = fiber;
_fiberReturnRequested = false;
_fiberReturnArgument = 0;
var hasContinuation = _continuations.TryGetValue(fiber, out var continuation);
bool callbackOk;
string? callbackError;
if (hasContinuation)
{
if (continuation.ArgOnRunAddress != 0 &&
!TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argOnRun))
if (!TryReadFiberFields(ctx, fiber, out fields))
{
callbackOk = false;
callbackError = $"failed to write resumed argOnRun to 0x{continuation.ArgOnRunAddress:X16}";
return SetReturn(ctx, FiberErrorInvalid);
}
if (fields.State != FiberStateIdle)
{
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
return SetReturn(ctx, FiberErrorState);
}
FiberContinuation targetContinuation;
if (_continuations.TryGetValue(fiber, out var savedContinuation))
{
targetContinuation = savedContinuation;
resumed = true;
}
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
{
return SetReturn(ctx, FiberErrorInvalid);
}
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
{
return SetReturn(ctx, FiberErrorInvalid);
}
var callerContinuation = new FiberContinuation(
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
outArgumentAddress);
if (previousFiber != 0)
{
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
previousState != FiberStateRun ||
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
{
return SetReturn(ctx, FiberErrorState);
}
_continuations[previousFiber] = callerContinuation;
_returnTargets[fiber] = new FiberReturnTarget(previousFiber, null);
}
else
{
callbackOk = scheduler.TryCallGuestContinuation(
ctx,
continuation.Context,
reason,
out callbackError);
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
}
}
else
{
callbackOk = scheduler.TryCallGuestFunction(
ctx,
fields.Entry,
fields.ArgOnInitialize,
argOnRun,
fields.ContextAddress,
fields.ContextSize,
reason,
out callbackError);
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
{
if (previousFiber != 0)
{
_continuations.TryRemove(previousFiber, out _);
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
}
_returnTargets.TryRemove(fiber, out _);
return SetReturn(ctx, FiberErrorInvalid);
}
if (resumed)
{
_continuations.TryRemove(fiber, out _);
}
transferTarget = targetContinuation.Context with { Rax = 0 };
}
var returnRequested = _fiberReturnRequested;
var returnArgument = _fiberReturnArgument;
if (!returnRequested && session.TryGetReturn(out var sessionReturnArgument))
{
returnRequested = true;
returnArgument = sessionReturnArgument;
}
if (!returnRequested)
{
_continuations.TryRemove(fiber, out _);
}
_runSessions.TryRemove(fiber, out _);
_currentFiberAddress = previousFiber;
_fiberReturnRequested = previousReturnRequested;
_fiberReturnArgument = previousReturnArgument;
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle);
if (switchingFromFiber)
{
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
}
if (!callbackOk)
{
TraceFiber($"run-failed fiber=0x{fiber:X16} entry=0x{fields.Entry:X16} error={callbackError}");
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP);
}
if (outArgumentAddress != 0 && !TryWriteUInt64(ctx, outArgumentAddress, returnArgument))
{
return SetReturn(ctx, FiberErrorInvalid);
}
TraceFiber($"run fiber=0x{fiber:X16} entry=0x{fields.Entry:X16} resume={hasContinuation} arg=0x{argOnRun:X16} ret=0x{returnArgument:X16}");
_currentFiberAddress = fiber;
_ = GuestThreadExecution.EnterFiber(fiber);
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
TraceFiber(
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
return SetReturn(ctx, 0);
}
private static GuestCpuContinuation CaptureContinuation(CpuContext ctx, ulong resumeRip, ulong resumeRsp) =>
private static bool TryCreateInitialContinuation(
CpuContext ctx,
FiberFields fields,
ulong argOnRun,
out FiberContinuation continuation)
{
continuation = default;
if (fields.ContextAddress == 0 || fields.ContextSize < FiberContextMinimumSize)
{
return false;
}
var stackEnd = fields.ContextAddress + fields.ContextSize;
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
if (!TryWriteUInt64(ctx, entryRsp, 0))
{
return false;
}
continuation = new FiberContinuation(
new GuestCpuContinuation(
fields.Entry,
entryRsp,
entryRsp,
ctx.Rflags == 0 ? 0x202UL : ctx.Rflags,
ctx.FsBase,
ctx.GsBase,
0,
0,
0,
0,
0,
argOnRun,
fields.ArgOnInitialize,
0,
0,
0,
0,
0,
0),
0);
return true;
}
private static bool TryWriteResumeArgument(
CpuContext ctx,
FiberContinuation continuation,
ulong argument) =>
continuation.ArgOnRunAddress == 0 ||
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
private static GuestCpuContinuation CaptureContinuation(
CpuContext ctx,
ulong resumeRip,
ulong resumeRsp,
ulong returnSlotAddress) =>
new(
resumeRip,
resumeRsp,
returnSlotAddress,
ctx.Rflags == 0 ? 0x202UL : ctx.Rflags,
ctx.FsBase,
ctx.GsBase,
@@ -665,6 +736,24 @@ public static class FiberExports
ctx[CpuRegister.R14],
ctx[CpuRegister.R15]);
private static ulong ResolveCurrentFiberAddress(CpuContext ctx)
{
if (_currentFiberAddress != 0)
{
return _currentFiberAddress;
}
if (GuestThreadExecution.CurrentFiberAddress != 0)
{
return GuestThreadExecution.CurrentFiberAddress;
}
return TryFindFiberByStack(ctx, out var fiberAddress) ? fiberAddress : 0;
}
internal static ulong GetCurrentFiberAddressForDiagnostics(CpuContext ctx) =>
ResolveCurrentFiberAddress(ctx);
private static int AttachContext(
CpuContext ctx,
ulong fiber,
@@ -928,30 +1017,9 @@ public static class FiberExports
GuestCpuContinuation Context,
ulong ArgOnRunAddress);
private sealed class FiberRunSession
{
private int _returnRequested;
private ulong _returnArgument;
public void SetReturn(ulong argument)
{
_returnArgument = argument;
Volatile.Write(ref _returnRequested, 1);
}
public bool TryGetReturn(out ulong argument)
{
if (Volatile.Read(ref _returnRequested) == 0)
{
argument = 0;
return false;
}
argument = _returnArgument;
return true;
}
}
private readonly record struct FiberReturnTarget(
ulong PreviousFiber,
FiberContinuation? ThreadContinuation);
private readonly record struct FiberStackRange(ulong Start, ulong Size)
{
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Ampr;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Threading;
@@ -10,8 +11,13 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelAprCompatExports
{
private static readonly ConcurrentDictionary<uint, ulong> _submittedCommandBuffers = new();
private static readonly ConcurrentDictionary<uint, AprSubmission> _submittedCommandBuffers = new();
private static int _nextSubmissionId;
private static int _aprWaitTraceCount;
private static readonly bool _traceApr =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
private readonly record struct AprSubmission(ulong CommandBuffer, ulong Priority, ulong ResultAddress);
[SysAbiExport(
Nid = "ASoW5WE-UPo",
@@ -36,7 +42,13 @@ public static class KernelAprCompatExports
submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
}
_submittedCommandBuffers[submissionId] = commandBuffer;
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, priority, resultAddress);
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return completionResult;
}
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
{
@@ -60,20 +72,23 @@ public static class KernelAprCompatExports
public static int KernelAprWaitCommandBuffer(CpuContext ctx)
{
var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]);
var priority = ctx[CpuRegister.Rsi];
var resultAddress = ctx[CpuRegister.Rdx];
var waitArg1 = ctx[CpuRegister.Rsi];
var waitArg2 = ctx[CpuRegister.Rdx];
if (!_submittedCommandBuffers.TryRemove(submissionId, out var commandBuffer))
if (!_submittedCommandBuffers.TryRemove(submissionId, out var submission))
{
TraceAprWaitFailure(ctx, "wait_missing", submissionId, commandBuffer: 0, waitArg1, waitArg2);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress);
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
{
TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceApr(ctx, "wait", submissionId, commandBuffer, priority, resultAddress);
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -91,7 +106,13 @@ public static class KernelAprCompatExports
}
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
_submittedCommandBuffers[submissionId] = commandBuffer;
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return completionResult;
}
TraceApr(ctx, "submit", submissionId, commandBuffer, ctx[CpuRegister.Rsi], 0);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -112,7 +133,13 @@ public static class KernelAprCompatExports
}
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
_submittedCommandBuffers[submissionId] = commandBuffer;
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
return completionResult;
}
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
{
@@ -130,6 +157,27 @@ public static class KernelAprCompatExports
return ctx.Memory.TryWrite(resultAddress, result);
}
private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress)
{
if (waitArg2 == 0)
{
return submittedResultAddress;
}
if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF)
{
return submittedResultAddress;
}
return waitArg2;
}
private static bool IsAmprCompletionToken(ulong value)
{
var tag = value >> 56;
return tag is 0x0C or 0x10;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
@@ -145,7 +193,7 @@ public static class KernelAprCompatExports
ulong priority,
ulong aux)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
if (!_traceApr)
{
return;
}
@@ -154,5 +202,54 @@ public static class KernelAprCompatExports
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] apr.{operation}: id=0x{submissionId:X8} cmd=0x{commandBuffer:X16} priority=0x{priority:X16} aux=0x{aux:X16} ret=0x{returnRip:X16}");
if (aux != 0 &&
ctx.TryReadUInt64(aux, out var result0) &&
ctx.TryReadUInt64(aux + sizeof(ulong), out var result1))
{
Console.Error.WriteLine(
$"[LOADER][TRACE] apr.{operation}.result: addr=0x{aux:X16} q0=0x{result0:X16} q1=0x{result1:X16}");
}
}
private static void TraceAprWaitFailure(
CpuContext ctx,
string operation,
uint submissionId,
ulong commandBuffer,
ulong priority,
ulong resultAddress)
{
if (!_traceApr)
{
return;
}
var traceCount = Interlocked.Increment(ref _aprWaitTraceCount);
if (traceCount > 32 && (traceCount & 0x3FF) != 0)
{
return;
}
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] apr.{operation}: id=0x{submissionId:X8} cmd=0x{commandBuffer:X16} " +
$"rsi=0x{priority:X16} rdx=0x{resultAddress:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16} " +
$"r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16} ret=0x{returnRip:X16}");
TraceReadableQword(ctx, operation, "rsi", priority);
TraceReadableQword(ctx, operation, "rdx", resultAddress);
TraceReadableQword(ctx, operation, "rcx", ctx[CpuRegister.Rcx]);
TraceReadableQword(ctx, operation, "r8", ctx[CpuRegister.R8]);
}
private static void TraceReadableQword(CpuContext ctx, string operation, string name, ulong address)
{
if (address == 0 || !ctx.TryReadUInt64(address, out var value))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] apr.{operation}.{name}: addr=0x{address:X16} q0=0x{value:X16}");
}
}
@@ -5,6 +5,7 @@ using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
namespace SharpEmu.Libs.Kernel;
@@ -113,6 +114,7 @@ public static class KernelEventFlagCompatExports
{
var handle = ctx[CpuRegister.Rdi];
var pattern = ctx[CpuRegister.Rsi];
var returnRip = GetCurrentReturnRip();
if (!_eventFlags.TryGetValue(handle, out var state))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
@@ -122,9 +124,10 @@ public static class KernelEventFlagCompatExports
{
state.Bits |= pattern;
Monitor.PulseAll(state.Gate);
TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16}");
TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
@@ -203,6 +206,7 @@ public static class KernelEventFlagCompatExports
var waitMode = unchecked((uint)ctx[CpuRegister.Rdx]);
var resultAddress = ctx[CpuRegister.Rcx];
var timeoutAddress = ctx[CpuRegister.R8];
var returnRip = GetCurrentReturnRip();
if (!_eventFlags.TryGetValue(handle, out var state))
{
@@ -223,7 +227,7 @@ public static class KernelEventFlagCompatExports
Monitor.Enter(state.Gate);
try
{
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, out var immediateWaitResult))
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
{
return SetReturn(ctx, immediateWaitResult);
}
@@ -232,11 +236,38 @@ public static class KernelEventFlagCompatExports
{
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec}");
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
if (!GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEventFlag"))
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var managedThread = Environment.CurrentManagedThreadId;
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEventFlag",
GetEventFlagWakeKey(handle),
() => (int)blockedWaitResult,
() =>
{
if (!TryPrepareBlockedWait(
ctx,
state,
pattern,
waitMode,
resultAddress,
out var preparedResult))
{
return false;
}
blockedWaitResult = preparedResult;
return true;
});
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
if (!requestedBlock)
{
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
@@ -245,7 +276,7 @@ public static class KernelEventFlagCompatExports
}
state.WaitingThreads++;
TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads}");
TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
var releaseWaiter = true;
try
{
@@ -261,11 +292,11 @@ public static class KernelEventFlagCompatExports
Monitor.Enter(state.Gate);
}
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, out var pumpedWaitResult))
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var pumpedWaitResult))
{
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false;
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
return SetReturn(ctx, pumpedWaitResult);
}
@@ -282,7 +313,7 @@ public static class KernelEventFlagCompatExports
}
state.WaitingThreads++;
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads}");
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
finally
@@ -360,19 +391,21 @@ public static class KernelEventFlagCompatExports
}
private static bool TryCompleteSatisfiedWait(
CpuContext ctx,
EventFlagState state,
ulong pattern,
uint waitMode,
out OrbisGen2Result result)
CpuContext ctx,
EventFlagState state,
ulong pattern,
uint waitMode,
ulong resultAddress,
out OrbisGen2Result result)
{
result = OrbisGen2Result.ORBIS_GEN2_OK;
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return false;
}
if (!TryWriteResultPattern(ctx, ctx[CpuRegister.Rcx], state.Bits))
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
return true;
@@ -382,6 +415,41 @@ public static class KernelEventFlagCompatExports
return true;
}
private static bool TryPrepareBlockedWait(
CpuContext ctx,
EventFlagState state,
ulong pattern,
uint waitMode,
ulong resultAddress,
out OrbisGen2Result result)
{
lock (state.Gate)
{
result = OrbisGen2Result.ORBIS_GEN2_OK;
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return false;
}
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
else
{
ApplyClearMode(state, pattern, waitMode);
}
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
TraceEventFlag(
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
return true;
}
}
private static string GetEventFlagWakeKey(ulong handle) =>
$"event_flag:0x{handle:X16}";
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
address == 0 || ctx.TryWriteUInt64(address, bits);
@@ -398,6 +466,32 @@ public static class KernelEventFlagCompatExports
return true;
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
return true;
}
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
{
Span<byte> buffer = stackalloc byte[1];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = buffer[0];
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
@@ -444,4 +538,101 @@ public static class KernelEventFlagCompatExports
Console.Error.WriteLine($"[LOADER][TRACE] event_flag.{message}");
}
}
private static ulong GetCurrentReturnRip() =>
GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)
? frame.ReturnRip
: 0UL;
private static string FormatFrameChain(CpuContext ctx)
{
Span<ulong> returns = stackalloc ulong[4];
var count = 0;
var frame = ctx[CpuRegister.Rbp];
for (var index = 0; index < returns.Length && frame != 0; index++)
{
if (!ctx.TryReadUInt64(frame, out var nextFrame) ||
!ctx.TryReadUInt64(frame + sizeof(ulong), out var returnAddress))
{
break;
}
returns[count++] = returnAddress;
if (nextFrame <= frame)
{
break;
}
frame = nextFrame;
}
return count switch
{
0 => "none",
1 => $"0x{returns[0]:X16}",
2 => $"0x{returns[0]:X16},0x{returns[1]:X16}",
3 => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16}",
_ => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16},0x{returns[3]:X16}",
};
}
private static string FormatGuestWaitObject(CpuContext ctx)
{
var r12 = ctx[CpuRegister.R12];
var r13 = ctx[CpuRegister.R13];
var objectAddress = r12 != 0
? r12
: r13 >= 0xA8
? r13 - 0xA8
: 0;
var builder = new StringBuilder(256);
builder.Append($"r12=0x{r12:X16} r13=0x{r13:X16}");
if (objectAddress == 0)
{
return builder.ToString();
}
builder.Append($" obj=0x{objectAddress:X16}");
AppendUInt32(builder, ctx, objectAddress + 0x58, "o58");
AppendUInt32(builder, ctx, objectAddress + 0x5C, "o5C");
AppendUInt64(builder, ctx, objectAddress + 0x60, "o60");
AppendByte(builder, ctx, objectAddress + 0x6C, "state6C");
AppendByte(builder, ctx, objectAddress + 0x6D, "o6D");
AppendByte(builder, ctx, objectAddress + 0xA0, "waitA0");
AppendByte(builder, ctx, objectAddress + 0xA1, "stateA1");
AppendByte(builder, ctx, objectAddress + 0xA2, "oA2");
AppendUInt64(builder, ctx, objectAddress + 0xA8, "eventA8");
if (r13 != 0)
{
AppendUInt64(builder, ctx, r13, "r13_0");
AppendUInt64(builder, ctx, r13 + 8, "r13_8");
}
return builder.ToString();
}
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadByte(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X2}");
}
}
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadUInt32(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X8}");
}
}
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (TryReadUInt64(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X16}");
}
}
}
@@ -10,10 +10,14 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelEventQueueCompatExports
{
private const int KernelEventSize = 0x20;
public const short KernelEventFilterGraphics = -14;
public const short KernelEventFilterAmpr = -16;
public const short KernelEventFilterAmprSystem = -17;
private static readonly object _eventQueueGate = new();
private static readonly HashSet<ulong> _eventQueues = new();
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
private static long _nextEventQueueHandle = 1;
public readonly record struct KernelQueuedEvent(
@@ -24,6 +28,11 @@ public static class KernelEventQueueCompatExports
ulong Data,
ulong UserData);
private readonly record struct KernelEventRegistration(
ulong Ident,
short Filter,
ulong UserData);
[SysAbiExport(
Nid = "D0OdFMjp46I",
ExportName = "sceKernelCreateEqueue",
@@ -42,6 +51,7 @@ public static class KernelEventQueueCompatExports
{
_eventQueues.Add(handle);
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
}
if (!ctx.TryWriteUInt64(outAddress, handle))
@@ -65,6 +75,7 @@ public static class KernelEventQueueCompatExports
{
_eventQueues.Remove(handle);
_pendingEvents.Remove(handle);
_registeredEvents.Remove(handle);
}
TraceEventQueue(ctx, "delete", handle);
@@ -122,8 +133,16 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelAddAmprEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "add_ampr", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var registered = RegisterEvent(
handle,
unchecked((uint)ctx[CpuRegister.Rsi]),
KernelEventFilterAmpr,
ctx[CpuRegister.Rdx]);
TraceEventQueue(ctx, "add_ampr", handle);
return registered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -133,8 +152,16 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelAddAmprSystemEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "add_ampr_system", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var registered = RegisterEvent(
handle,
unchecked((uint)ctx[CpuRegister.Rsi]),
KernelEventFilterAmprSystem,
ctx[CpuRegister.Rdx]);
TraceEventQueue(ctx, "add_ampr_system", handle);
return registered
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -144,8 +171,15 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelDeleteAmprEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "delete_ampr", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var deleted = DeleteRegisteredEvent(
handle,
unchecked((uint)ctx[CpuRegister.Rsi]),
KernelEventFilterAmpr);
TraceEventQueue(ctx, "delete_ampr", handle);
return deleted
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -155,8 +189,15 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")]
public static int KernelDeleteAmprSystemEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "delete_ampr_system", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
var handle = ctx[CpuRegister.Rdi];
var deleted = DeleteRegisteredEvent(
handle,
unchecked((uint)ctx[CpuRegister.Rsi]),
KernelEventFilterAmprSystem);
TraceEventQueue(ctx, "delete_ampr_system", handle);
return deleted
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
[SysAbiExport(
@@ -171,6 +212,57 @@ public static class KernelEventQueueCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "vz+pg2zdopI",
ExportName = "sceKernelGetEventUserData",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelGetEventUserData(CpuContext ctx)
{
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x18, out var userData);
ctx[CpuRegister.Rax] = userData;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "mJ7aghmgvfc",
ExportName = "sceKernelGetEventId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelGetEventId(CpuContext ctx)
{
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi], out var ident);
ctx[CpuRegister.Rax] = ident;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "23CPPI1tyBY",
ExportName = "sceKernelGetEventFilter",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelGetEventFilter(CpuContext ctx)
{
Span<byte> filterBytes = stackalloc byte[sizeof(short)];
var filter = ctx.Memory.TryRead(ctx[CpuRegister.Rdi] + 0x08, filterBytes)
? BinaryPrimitives.ReadInt16LittleEndian(filterBytes)
: (short)0;
ctx[CpuRegister.Rax] = unchecked((uint)filter);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "kwGyyjohI50",
ExportName = "sceKernelGetEventData",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelGetEventData(CpuContext ctx)
{
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x10, out var data);
ctx[CpuRegister.Rax] = data;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "fzyMKs9kim0",
ExportName = "sceKernelWaitEqueue",
@@ -196,7 +288,13 @@ public static class KernelEventQueueCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue"))
if (timeoutAddress == 0 &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"sceKernelWaitEqueue",
GetEventQueueWakeKey(handle),
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress),
() => HasPendingEvents(handle)))
{
TraceEventQueue(ctx, "wait-block", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -216,6 +314,7 @@ public static class KernelEventQueueCompatExports
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
{
var queued = false;
lock (_eventQueueGate)
{
if (!_eventQueues.Contains(handle))
@@ -230,10 +329,100 @@ public static class KernelEventQueueCompatExports
}
queue.AddLast(queuedEvent);
queued = true;
}
if (queued)
{
WakeEventQueue(handle);
}
return queued;
}
public static bool RegisterEvent(
ulong handle,
ulong ident,
short filter,
ulong userData)
{
lock (_eventQueueGate)
{
if (!_eventQueues.Contains(handle))
{
return false;
}
if (!_registeredEvents.TryGetValue(handle, out var events))
{
events = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
_registeredEvents[handle] = events;
}
events[(ident, filter)] = new KernelEventRegistration(ident, filter, userData);
return true;
}
}
public static bool DeleteRegisteredEvent(
ulong handle,
ulong ident,
short filter)
{
lock (_eventQueueGate)
{
return _registeredEvents.TryGetValue(handle, out var events) &&
events.Remove((ident, filter));
}
}
public static int TriggerRegisteredEvents(
ulong ident,
short filter,
ulong data)
{
List<ulong>? wakeHandles = null;
var triggeredCount = 0;
lock (_eventQueueGate)
{
foreach (var (handle, registrations) in _registeredEvents)
{
if (!registrations.TryGetValue((ident, filter), out var registration))
{
continue;
}
if (!_pendingEvents.TryGetValue(handle, out var queue))
{
queue = new LinkedList<KernelQueuedEvent>();
_pendingEvents[handle] = queue;
}
QueueOrUpdateEvent(
queue,
new KernelQueuedEvent(
registration.Ident,
registration.Filter,
0,
1,
data,
registration.UserData));
(wakeHandles ??= new List<ulong>()).Add(handle);
triggeredCount++;
}
}
if (wakeHandles is not null)
{
foreach (var handle in wakeHandles)
{
WakeEventQueue(handle);
}
}
return triggeredCount;
}
public static bool TriggerDisplayEvent(
ulong handle,
ulong ident,
@@ -241,6 +430,7 @@ public static class KernelEventQueueCompatExports
ulong eventHint,
ulong userData)
{
var triggered = false;
lock (_eventQueueGate)
{
if (!_eventQueues.Contains(handle))
@@ -254,17 +444,8 @@ public static class KernelEventQueueCompatExports
_pendingEvents[handle] = events;
}
LinkedListNode<KernelQueuedEvent>? pendingNode = null;
for (var node = events.First; node is not null; node = node.Next)
{
if (node.Value.Ident == ident && node.Value.Filter == filter)
{
pendingNode = node;
break;
}
}
var count = 1UL;
var pendingNode = FindPendingEvent(events, ident, filter);
if (pendingNode is not null)
{
count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL);
@@ -289,8 +470,80 @@ public static class KernelEventQueueCompatExports
events.AddLast(triggeredEvent);
}
return true;
triggered = true;
}
if (triggered)
{
WakeEventQueue(handle);
}
return triggered;
}
private static int ResumeWaitEqueue(
CpuContext ctx,
ulong handle,
ulong eventsAddress,
int eventCapacity,
ulong outCountAddress)
{
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool HasPendingEvents(ulong handle)
{
lock (_eventQueueGate)
{
return _pendingEvents.TryGetValue(handle, out var events) && events.Count != 0;
}
}
private static void QueueOrUpdateEvent(
LinkedList<KernelQueuedEvent> queue,
KernelQueuedEvent queuedEvent)
{
var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter);
if (pendingNode is null)
{
queue.AddLast(queuedEvent);
return;
}
pendingNode.Value = queuedEvent with
{
Fflags = Math.Max(pendingNode.Value.Fflags + 1, queuedEvent.Fflags),
};
}
private static LinkedListNode<KernelQueuedEvent>? FindPendingEvent(
LinkedList<KernelQueuedEvent> queue,
ulong ident,
short filter)
{
for (var node = queue.First; node is not null; node = node.Next)
{
if (node.Value.Ident == ident && node.Value.Filter == filter)
{
return node;
}
}
return null;
}
private static string GetEventQueueWakeKey(ulong handle) =>
$"sceKernelWaitEqueue:{handle:X16}";
private static void WakeEventQueue(ulong handle)
{
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventQueueWakeKey(handle));
}
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
+26
View File
@@ -252,6 +252,32 @@ public static class KernelExports
return PthreadCreate(ctx);
}
[SysAbiExport(
Nid = "3kg7rT0NQIs",
ExportName = "scePthreadExit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadExit(CpuContext ctx)
{
var value = ctx[CpuRegister.Rdi];
GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value);
ctx[CpuRegister.Rax] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "FJrT5LuUBAU",
ExportName = "pthread_exit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePosix")]
public static int PosixPthreadExit(CpuContext ctx)
{
var value = ctx[CpuRegister.Rdi];
GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value);
ctx[CpuRegister.Rax] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "onNY9Byn-W8",
ExportName = "scePthreadJoin",
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Ampr;
using System.Buffers;
using System.Buffers.Binary;
using System.Text;
using System.Threading;
@@ -64,6 +65,7 @@ public static class KernelMemoryCompatExports
private const uint HostPageExecuteWriteCopy = 0x80;
private const uint HostPageGuard = 0x100;
private const int Enomem = 12;
private const int Efault = 14;
private const int Einval = 22;
private const int Erange = 34;
private const int Struncate = 80;
@@ -95,10 +97,14 @@ public static class KernelMemoryCompatExports
private static readonly object _libcAllocGate = new();
private static readonly object _memoryGate = new();
private static readonly object _tlsGate = new();
private static readonly object _ioTraceGate = new();
private static readonly object _statCacheGate = new();
private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new();
private static readonly Dictionary<ulong, LibcHeapAllocation> _libcAllocations = new();
private static readonly Dictionary<ulong, MappedRegion> _mappedRegions = new();
private static readonly Dictionary<ulong, ulong> _tlsModuleBlocks = new();
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
private static long _nextFileDescriptor = 2;
private static ulong _nextPhysicalAddress;
private static ulong _nextVirtualAddress;
@@ -113,6 +119,8 @@ public static class KernelMemoryCompatExports
private static int _hostMemoryWriteFallbackCount;
private static int _hostMemoryReadFallbackCount;
private static int _nullWcscpyRecoveryCount;
private static string? _cachedApp0Root;
private static string? _cachedDownload0Root;
[StructLayout(LayoutKind.Sequential)]
private struct MemoryBasicInformation
@@ -164,7 +172,7 @@ public static class KernelMemoryCompatExports
var desiredAddress = AlignUp(
_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress,
effectiveAlignment);
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, OrbisProtCpuReadWrite, out address) ||
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, OrbisProtCpuReadWrite, effectiveAlignment, out address) ||
address == 0)
{
return false;
@@ -597,7 +605,6 @@ public static class KernelMemoryCompatExports
ulong NextGpArg() => vaCursor.NextGpArg();
double NextFloatArg() => vaCursor.NextFloatArg();
rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
vaCursor.Commit();
}
Console.Write(rendered);
@@ -1236,6 +1243,12 @@ public static class KernelMemoryCompatExports
var mode = ResolveOpenMode(flags, access);
try
{
if (IsMutatingOpen(flags) && IsReadOnlyGuestMutationPath(guestPath))
{
LogOpenTrace($"_open readonly path='{guestPath}' host='{hostPath}' flags=0x{flags:X8}");
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
var wantsDirectory = (flags & O_DIRECTORY) != 0;
if (wantsDirectory || Directory.Exists(hostPath))
{
@@ -1280,6 +1293,11 @@ public static class KernelMemoryCompatExports
_openFiles[fd] = stream;
}
if (IsMutatingOpen(flags))
{
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
}
LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}");
ctx[CpuRegister.Rax] = unchecked((ulong)fd);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -1334,11 +1352,30 @@ public static class KernelMemoryCompatExports
}
var hostPath = ResolveGuestPath(guestPath);
if (!TryWriteHostPathStat(ctx, statAddress, hostPath))
var statCacheKey = GetNegativeStatCacheKey(guestPath);
if (statCacheKey is not null && IsNegativeStatCached(statCacheKey))
{
LogUniqueStatTrace(guestPath, hostPath, found: false);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (!TryWriteHostPathStat(ctx, statAddress, hostPath))
{
if (statCacheKey is not null)
{
AddNegativeStatCache(statCacheKey);
}
LogUniqueStatTrace(guestPath, hostPath, found: false);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (statCacheKey is not null)
{
RemoveNegativeStatCache(statCacheKey);
}
LogUniqueStatTrace(guestPath, hostPath, found: true);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1356,13 +1393,22 @@ public static class KernelMemoryCompatExports
var sizesAddress = ctx[CpuRegister.Rcx];
if (pathListAddress == 0 || count == 0 || sizesAddress == 0 || count > 1024)
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
for (ulong i = 0; i < count; i++)
{
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), uint.MaxValue))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var guestPath))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -1370,6 +1416,7 @@ public static class KernelMemoryCompatExports
if (!TryGetAprFileSize(hostPath, out var fileSize))
{
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} result=not_found");
KernelRuntimeCompatExports.TrySetErrno(ctx, 2);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
@@ -1379,11 +1426,13 @@ public static class KernelMemoryCompatExports
if (idsAddress != 0 &&
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize))
{
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
@@ -1415,6 +1464,170 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "AUXVxWeJU-A",
ExportName = "sceKernelUnlink",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelUnlink(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
if (pathAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var hostPath = ResolveGuestPath(guestPath);
if (IsReadOnlyGuestMutationPath(guestPath))
{
LogOpenTrace($"unlink readonly path='{guestPath}' host='{hostPath}'");
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
try
{
if (Directory.Exists(hostPath))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
if (!File.Exists(hostPath))
{
AddNegativeStatCacheForGuestPath(guestPath);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
File.Delete(hostPath);
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
AddNegativeStatCacheForGuestPath(guestPath);
LogOpenTrace($"unlink path='{guestPath}' host='{hostPath}'");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (UnauthorizedAccessException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
}
[SysAbiExport(
Nid = "1-LFLmRFxxM",
ExportName = "sceKernelMkdir",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelMkdir(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
if (pathAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var hostPath = ResolveGuestPath(guestPath);
if (IsReadOnlyGuestMutationPath(guestPath))
{
LogOpenTrace($"mkdir readonly path='{guestPath}' host='{hostPath}'");
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
try
{
if (File.Exists(hostPath) || Directory.Exists(hostPath))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
}
var parentDirectory = Path.GetDirectoryName(hostPath);
if (string.IsNullOrWhiteSpace(parentDirectory) || !Directory.Exists(parentDirectory))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
Directory.CreateDirectory(hostPath);
if (!Directory.Exists(hostPath))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
LogOpenTrace($"mkdir path='{guestPath}' host='{hostPath}'");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (UnauthorizedAccessException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
}
[SysAbiExport(
Nid = "naInUjYt3so",
ExportName = "sceKernelRmdir",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelRmdir(CpuContext ctx)
{
var pathAddress = ctx[CpuRegister.Rdi];
if (pathAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryReadNullTerminatedUtf8(ctx, pathAddress, MaxGuestStringLength, out var guestPath))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var hostPath = ResolveGuestPath(guestPath);
if (IsReadOnlyGuestMutationPath(guestPath))
{
LogOpenTrace($"rmdir readonly path='{guestPath}' host='{hostPath}'");
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
try
{
if (!Directory.Exists(hostPath))
{
AddNegativeStatCacheForGuestPath(guestPath);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
Directory.Delete(hostPath, recursive: false);
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
AddNegativeStatCacheForGuestPath(guestPath);
LogOpenTrace($"rmdir path='{guestPath}' host='{hostPath}'");
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
catch (UnauthorizedAccessException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
}
private static int KernelCloseCore(CpuContext ctx, int fd)
{
if (fd is 0 or 1 or 2)
@@ -2242,7 +2455,7 @@ public static class KernelMemoryCompatExports
}
else
{
reserved = TryReserveGuestVirtualRange(ctx, desiredAddress, length, protection, out mappedAddress);
reserved = TryReserveGuestVirtualRange(ctx, desiredAddress, length, protection, effectiveAlignment, out mappedAddress);
}
if (ShouldTraceDirectMemory())
{
@@ -2331,7 +2544,7 @@ public static class KernelMemoryCompatExports
{
mappedAddress = requestedAddress;
}
else if (!TryReserveGuestVirtualRange(ctx, desiredAddress, length, protection, out mappedAddress))
else if (!TryReserveGuestVirtualRange(ctx, desiredAddress, length, protection, OrbisPageSize, out mappedAddress))
{
mappedAddress = requestedAddress != 0 && fixedMapping
? requestedAddress
@@ -2721,7 +2934,6 @@ public static class KernelMemoryCompatExports
ulong NextGpArg() => vaCursor.NextGpArg();
double NextFloatArg() => vaCursor.NextFloatArg();
var rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
vaCursor.Commit();
var outputBytes = Encoding.UTF8.GetBytes(rendered);
return WriteSnprintfOutput(ctx, destination, bufferSize, outputBytes);
@@ -2768,7 +2980,6 @@ public static class KernelMemoryCompatExports
ulong NextGpArg() => vaCursor.NextGpArg();
double NextFloatArg() => vaCursor.NextFloatArg();
rendered = FormatString(ctx, format, NextGpArg, NextFloatArg);
vaCursor.Commit();
}
TraceWidePrintf(ctx, "vswprintf", destination, bufferSize, format, rendered);
@@ -3552,13 +3763,6 @@ public static class KernelMemoryCompatExports
: 0.0;
}
public void Commit()
{
_ = TryWriteUInt32Compat(_ctx, _vaListAddress + 0, _gpOffset);
_ = TryWriteUInt32Compat(_ctx, _vaListAddress + 4, _fpOffset);
_ = TryWriteUInt64Compat(_ctx, _vaListAddress + 8, _overflowArgArea);
}
public uint GpOffset => _gpOffset;
public uint FpOffset => _fpOffset;
@@ -3613,6 +3817,7 @@ public static class KernelMemoryCompatExports
ulong desiredAddress,
ulong length,
int protection,
ulong alignment,
out ulong mappedAddress)
{
mappedAddress = 0;
@@ -3625,40 +3830,51 @@ public static class KernelMemoryCompatExports
{
object memoryObject = ctx.Memory;
MethodInfo? allocateAt = null;
MethodInfo? allocateAtOrAbove = null;
var allocateAtHasAllowAlternativeArg = false;
for (var depth = 0; depth < 4; depth++)
{
foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
if (!string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
{
continue;
}
var parameters = candidate.GetParameters();
if (parameters.Length == 3 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool))
{
allocateAt = candidate;
allocateAtHasAllowAlternativeArg = false;
break;
}
if (parameters.Length == 4 &&
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
parameters.Length == 5 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool) &&
parameters[3].ParameterType == typeof(bool))
parameters[3].ParameterType == typeof(ulong) &&
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
{
allocateAtOrAbove = candidate;
}
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
{
if (parameters.Length == 3 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool))
{
allocateAt = candidate;
allocateAtHasAllowAlternativeArg = false;
}
else if (parameters.Length == 4 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool) &&
parameters[3].ParameterType == typeof(bool))
{
allocateAt = candidate;
allocateAtHasAllowAlternativeArg = true;
}
}
if (allocateAtOrAbove is not null && allocateAt is not null)
{
allocateAt = candidate;
allocateAtHasAllowAlternativeArg = true;
break;
}
}
if (allocateAt is not null)
if (allocateAtOrAbove is not null || allocateAt is not null)
{
break;
}
@@ -3678,15 +3894,27 @@ public static class KernelMemoryCompatExports
memoryObject = innerValue;
}
var executable = (protection & OrbisProtCpuExec) != 0;
if (allocateAtOrAbove is not null)
{
var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL };
var searchResult = allocateAtOrAbove.Invoke(memoryObject, searchArgs);
if (searchResult is bool trueValue && trueValue &&
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
{
mappedAddress = searchedAddress;
return true;
}
}
if (allocateAt is null)
{
Console.Error.WriteLine($"[LOADER][TRACE] reserve range: AllocateAt missing on {ctx.Memory.GetType().FullName}");
return false;
}
var executable = (protection & OrbisProtCpuExec) != 0;
var invokeArgs = allocateAtHasAllowAlternativeArg
? new object[] { desiredAddress, length, executable, true }
? new object[] { desiredAddress, length, executable, false }
: new object[] { desiredAddress, length, executable };
var result = allocateAt.Invoke(memoryObject, invokeArgs);
if (result is not ulong allocated || allocated == 0)
@@ -3783,57 +4011,72 @@ public static class KernelMemoryCompatExports
return guestPath;
}
var devlogAppRoot = ResolveDevlogAppRoot();
if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase))
{
var relative = NormalizeMountRelativePath(guestPath["/devlog/app/".Length..]);
return Path.Combine(devlogAppRoot, relative);
return Path.Combine(ResolveDevlogAppRoot(), relative);
}
if (guestPath.StartsWith("devlog/app/", StringComparison.OrdinalIgnoreCase))
{
var relative = NormalizeMountRelativePath(guestPath["devlog/app/".Length..]);
return Path.Combine(devlogAppRoot, relative);
return Path.Combine(ResolveDevlogAppRoot(), relative);
}
if (string.Equals(guestPath, "/devlog/app", StringComparison.OrdinalIgnoreCase) ||
string.Equals(guestPath, "devlog/app", StringComparison.OrdinalIgnoreCase))
{
return devlogAppRoot;
return ResolveDevlogAppRoot();
}
var temp0Root = ResolveTemp0Root();
if (guestPath.StartsWith("/temp0/", StringComparison.OrdinalIgnoreCase))
{
var relative = NormalizeMountRelativePath(guestPath["/temp0/".Length..]);
return Path.Combine(temp0Root, relative);
return Path.Combine(ResolveTemp0Root(), relative);
}
if (string.Equals(guestPath, "/temp0", StringComparison.OrdinalIgnoreCase))
{
return temp0Root;
return ResolveTemp0Root();
}
if (guestPath.StartsWith("/download0/", StringComparison.OrdinalIgnoreCase))
{
var relative = NormalizeMountRelativePath(guestPath["/download0/".Length..]);
return Path.Combine(ResolveDownload0Root(), relative);
}
if (guestPath.StartsWith("download0/", StringComparison.OrdinalIgnoreCase))
{
var relative = NormalizeMountRelativePath(guestPath["download0/".Length..]);
return Path.Combine(ResolveDownload0Root(), relative);
}
if (string.Equals(guestPath, "/download0", StringComparison.OrdinalIgnoreCase) ||
string.Equals(guestPath, "download0", StringComparison.OrdinalIgnoreCase))
{
return ResolveDownload0Root();
}
var hostappRoot = ResolveHostappRoot();
if (guestPath.StartsWith("/hostapp/", StringComparison.OrdinalIgnoreCase))
{
var relative = NormalizeMountRelativePath(guestPath["/hostapp/".Length..]);
return Path.Combine(hostappRoot, relative);
return Path.Combine(ResolveHostappRoot(), relative);
}
if (guestPath.StartsWith("hostapp/", StringComparison.OrdinalIgnoreCase))
{
var relative = NormalizeMountRelativePath(guestPath["hostapp/".Length..]);
return Path.Combine(hostappRoot, relative);
return Path.Combine(ResolveHostappRoot(), relative);
}
if (string.Equals(guestPath, "/hostapp", StringComparison.OrdinalIgnoreCase) ||
string.Equals(guestPath, "hostapp", StringComparison.OrdinalIgnoreCase))
{
return hostappRoot;
return ResolveHostappRoot();
}
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
var app0Root = ResolveApp0Root();
if (!string.IsNullOrWhiteSpace(app0Root))
{
if (string.Equals(guestPath, "/app0", StringComparison.OrdinalIgnoreCase) ||
@@ -3866,6 +4109,24 @@ public static class KernelMemoryCompatExports
return guestPath;
}
private static string? ResolveApp0Root()
{
var cached = Volatile.Read(ref _cachedApp0Root);
if (!string.IsNullOrWhiteSpace(cached))
{
return cached;
}
var configured = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
if (string.IsNullOrWhiteSpace(configured))
{
return null;
}
Interlocked.CompareExchange(ref _cachedApp0Root, configured, null);
return _cachedApp0Root;
}
private static string NormalizeMountRelativePath(string relativePath)
{
return relativePath
@@ -3916,6 +4177,32 @@ public static class KernelMemoryCompatExports
return root;
}
private static string ResolveDownload0Root()
{
var cached = Volatile.Read(ref _cachedDownload0Root);
if (!string.IsNullOrWhiteSpace(cached))
{
return cached;
}
const string download0VariableName = "SHARPEMU_DOWNLOAD0_DIR";
var configuredRoot = Environment.GetEnvironmentVariable(download0VariableName);
string root;
if (!string.IsNullOrWhiteSpace(configuredRoot))
{
root = Path.GetFullPath(configuredRoot);
}
else
{
root = Path.Combine(GetPerAppWritableRoot(), "download0");
Environment.SetEnvironmentVariable(download0VariableName, root);
}
Directory.CreateDirectory(root);
Interlocked.CompareExchange(ref _cachedDownload0Root, root, null);
return _cachedDownload0Root;
}
private static string ResolveHostappRoot()
{
const string hostappVariableName = "SHARPEMU_HOSTAPP_DIR";
@@ -3935,6 +4222,22 @@ public static class KernelMemoryCompatExports
return root;
}
private static string GetPerAppWritableRoot()
{
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
var appName = string.IsNullOrWhiteSpace(app0Root)
? "default"
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
if (string.IsNullOrWhiteSpace(appName))
{
appName = "default";
}
var invalidChars = Path.GetInvalidFileNameChars();
appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
return Path.Combine(Path.GetTempPath(), "SharpEmu", appName);
}
private static void EnsureOpenParentDirectoryExists(string guestPath, string hostPath, int flags)
{
if (string.IsNullOrWhiteSpace(hostPath))
@@ -3958,7 +4261,28 @@ public static class KernelMemoryCompatExports
}
}
private static bool IsMutatingOpen(int flags) =>
(flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0;
private static bool IsReadOnlyGuestMutationPath(string guestPath)
{
var normalized = NormalizeGuestStatCachePath(guestPath);
return normalized is not null &&
(string.Equals(normalized, "/app0", StringComparison.OrdinalIgnoreCase) ||
normalized.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase));
}
private static bool TryReadCString(CpuContext ctx, ulong address, ulong maxLength, out byte[] bytes)
{
return TryReadBytesUntilNull(ctx, address, maxLength, 1_048_576, out bytes);
}
private static bool TryReadBytesUntilNull(
CpuContext ctx,
ulong address,
ulong maxLength,
int hardLimit,
out byte[] bytes)
{
bytes = Array.Empty<byte>();
if (address == 0)
@@ -3966,26 +4290,65 @@ public static class KernelMemoryCompatExports
return false;
}
var limit = (int)Math.Min(maxLength, 1_048_576UL);
var buffer = new List<byte>(Math.Min(limit, 256));
Span<byte> one = stackalloc byte[1];
for (var i = 0; i < limit; i++)
var limit = (int)Math.Min(maxLength, (ulong)Math.Max(0, hardLimit));
if (limit == 0)
{
if (!TryReadCompat(ctx, address + (ulong)i, one))
return true;
}
const int maxChunkSize = 4096;
var chunk = GC.AllocateUninitializedArray<byte>(Math.Min(maxChunkSize, limit));
var writer = new ArrayBufferWriter<byte>(Math.Min(limit, 256));
ulong offset = 0;
while (offset < (ulong)limit)
{
var current = address + offset;
if (current < address)
{
return false;
}
var pageRemaining = maxChunkSize - (int)(current & (maxChunkSize - 1));
var remaining = (int)Math.Min((ulong)limit - offset, (ulong)Math.Min(chunk.Length, pageRemaining));
var span = chunk.AsSpan(0, remaining);
if (TryReadCompat(ctx, current, span))
{
var nulIndex = span.IndexOf((byte)0);
var copyLength = nulIndex >= 0 ? nulIndex : remaining;
if (copyLength > 0)
{
span[..copyLength].CopyTo(writer.GetSpan(copyLength));
writer.Advance(copyLength);
}
if (nulIndex >= 0)
{
bytes = writer.WrittenSpan.ToArray();
return true;
}
offset += (ulong)remaining;
continue;
}
Span<byte> one = stackalloc byte[1];
if (!TryReadCompat(ctx, current, one))
{
return false;
}
if (one[0] == 0)
{
bytes = buffer.ToArray();
bytes = writer.WrittenSpan.ToArray();
return true;
}
buffer.Add(one[0]);
one.CopyTo(writer.GetSpan(1));
writer.Advance(1);
offset++;
}
bytes = buffer.ToArray();
bytes = writer.WrittenSpan.ToArray();
return true;
}
@@ -4165,25 +4528,12 @@ public static class KernelMemoryCompatExports
return false;
}
var buffer = new List<byte>(Math.Min(maxLength, 256));
Span<byte> one = stackalloc byte[1];
for (var i = 0; i < maxLength; i++)
if (!TryReadBytesUntilNull(ctx, address, (ulong)maxLength, maxLength, out var bytes))
{
if (!TryReadCompat(ctx, address + (ulong)i, one))
{
return false;
}
if (one[0] == 0)
{
value = Encoding.UTF8.GetString(buffer.ToArray());
return true;
}
buffer.Add(one[0]);
return false;
}
value = Encoding.UTF8.GetString(buffer.ToArray());
value = Encoding.UTF8.GetString(bytes);
return true;
}
@@ -4299,7 +4649,7 @@ public static class KernelMemoryCompatExports
return true;
}
private static bool TryReadUInt64Compat(CpuContext ctx, ulong address, out ulong value)
internal static bool TryReadUInt64Compat(CpuContext ctx, ulong address, out ulong value)
{
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
if (!TryReadCompat(ctx, address, bytes))
@@ -4346,7 +4696,7 @@ public static class KernelMemoryCompatExports
return TryWriteCompat(ctx, address, bytes);
}
private static bool TryWriteUInt64Compat(CpuContext ctx, ulong address, ulong value)
internal static bool TryWriteUInt64Compat(CpuContext ctx, ulong address, ulong value)
{
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(bytes, value);
@@ -5294,19 +5644,20 @@ public static class KernelMemoryCompatExports
size = 0;
try
{
if (Directory.Exists(hostPath))
var fileInfo = new FileInfo(hostPath);
if (fileInfo.Exists)
{
size = 65536;
var length = fileInfo.Length;
size = length < 0 ? 0UL : unchecked((ulong)length);
return true;
}
if (!File.Exists(hostPath))
if (!new DirectoryInfo(hostPath).Exists)
{
return false;
}
var length = new FileInfo(hostPath).Length;
size = length < 0 ? 0UL : unchecked((ulong)length);
size = 65536;
return true;
}
catch
@@ -5491,6 +5842,120 @@ public static class KernelMemoryCompatExports
Console.Error.WriteLine($"[LOADER][TRACE] {operation} path='{path}' {detail}");
}
private static void LogUniqueStatTrace(string guestPath, string hostPath, bool found)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IO"), "1", StringComparison.Ordinal))
{
return;
}
var result = found ? "found" : "not_found";
lock (_ioTraceGate)
{
if (!_tracedStatResults.Add($"{result}\0{guestPath}"))
{
return;
}
}
LogIoTrace("stat", guestPath, $"host='{hostPath}' result={result}");
}
private static string? GetNegativeStatCacheKey(string guestPath)
{
var normalized = NormalizeGuestStatCachePath(guestPath);
return IsReadOnlyGuestStatPath(normalized) ? normalized : null;
}
private static string? NormalizeGuestStatCachePath(string guestPath)
{
var normalized = guestPath.Replace('\\', '/').TrimEnd('/');
if (normalized.Length == 0)
{
return null;
}
if (normalized[0] != '/')
{
normalized = "/" + normalized;
}
return normalized;
}
private static bool IsReadOnlyGuestStatPath(string? normalizedGuestPath) =>
normalizedGuestPath is not null &&
(string.Equals(normalizedGuestPath, "/app0", StringComparison.OrdinalIgnoreCase) ||
normalizedGuestPath.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(normalizedGuestPath, "/hostapp", StringComparison.OrdinalIgnoreCase) ||
normalizedGuestPath.StartsWith("/hostapp/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(normalizedGuestPath, "/devlog/app", StringComparison.OrdinalIgnoreCase) ||
normalizedGuestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(normalizedGuestPath, "/temp0", StringComparison.OrdinalIgnoreCase) ||
normalizedGuestPath.StartsWith("/temp0/", StringComparison.OrdinalIgnoreCase) ||
string.Equals(normalizedGuestPath, "/download0", StringComparison.OrdinalIgnoreCase) ||
normalizedGuestPath.StartsWith("/download0/", StringComparison.OrdinalIgnoreCase));
private static bool IsNegativeStatCached(string cacheKey)
{
lock (_statCacheGate)
{
return _negativeStatCache.Contains(cacheKey);
}
}
private static void AddNegativeStatCache(string cacheKey)
{
lock (_statCacheGate)
{
_negativeStatCache.Add(cacheKey);
}
}
private static void RemoveNegativeStatCache(string cacheKey)
{
lock (_statCacheGate)
{
_negativeStatCache.Remove(cacheKey);
}
}
private static void AddNegativeStatCacheForGuestPath(string guestPath)
{
var cacheKey = GetNegativeStatCacheKey(guestPath);
if (cacheKey is not null)
{
AddNegativeStatCache(cacheKey);
}
}
private static void InvalidateNegativeStatCacheForPathAndAncestors(string guestPath)
{
var normalized = NormalizeGuestStatCachePath(guestPath);
if (normalized is null)
{
return;
}
lock (_statCacheGate)
{
var current = normalized;
while (true)
{
_negativeStatCache.Remove(current);
var slash = current.LastIndexOf('/');
if (slash <= 0)
{
break;
}
current = current[..slash];
}
_negativeStatCache.Remove("/");
}
}
private static string PreviewIoBytes(byte[] buffer, int count, int maxBytes)
{
if (count <= 0)
@@ -1,7 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
@@ -9,7 +11,7 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelPthreadCompatExports
{
private const int MutexTypeDefault = 1;
private const int MutexTypeDefault = 0;
private const int MutexTypeErrorCheck = 1;
private const int MutexTypeRecursive = 2;
private const int MutexTypeNormal = 3;
@@ -19,12 +21,23 @@ public static class KernelPthreadCompatExports
private const int MutexAttrObjectSize = 0x40;
private const int CondObjectSize = 0x100;
private const int DefaultSpuriousCondWakeMilliseconds = 1;
private const int PthreadOnceUninitialized = 0;
private const int PthreadOnceInProgress = 1;
private const int PthreadOnceDone = 2;
private static readonly object _stateGate = new();
private static readonly Dictionary<ulong, PthreadMutexState> _mutexStates = new();
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
private static readonly Dictionary<ulong, object> _onceGates = new();
private static readonly HashSet<ulong> _condAttrStates = new();
private static readonly bool _tracePthreads =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
private static readonly bool _tracePthreadConds =
_tracePthreads ||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
private sealed class PthreadMutexState
{
@@ -35,6 +48,12 @@ public static class KernelPthreadCompatExports
public int Protocol { get; set; }
}
private sealed class PthreadMutexWaiter
{
public required ulong ThreadId { get; init; }
public int Reserved;
}
private sealed class PthreadCondState
{
public object SyncRoot { get; } = new();
@@ -57,6 +76,13 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "EotR8a3ASf4",
ExportName = "pthread_self",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadSelf(CpuContext ctx) => PthreadSelf(ctx);
[SysAbiExport(
Nid = "3PtV6p3QNX4",
ExportName = "scePthreadEqual",
@@ -340,6 +366,93 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "14bOACANTBo",
ExportName = "scePthreadOnce",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadOnce(CpuContext ctx)
{
var onceAddress = ctx[CpuRegister.Rdi];
var initRoutine = ctx[CpuRegister.Rsi];
if (onceAddress == 0 || initRoutine == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!TryReadInt32(ctx, onceAddress, out var onceValue))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (onceValue == PthreadOnceDone)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
var gate = GetPthreadOnceGate(onceAddress);
var shouldCall = false;
lock (gate)
{
if (!TryReadInt32(ctx, onceAddress, out onceValue))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
while (onceValue == PthreadOnceInProgress)
{
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
if (!TryReadInt32(ctx, onceAddress, out onceValue))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
if (onceValue != PthreadOnceDone)
{
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceInProgress))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
shouldCall = true;
}
}
if (shouldCall)
{
var scheduler = GuestThreadExecution.Scheduler;
string? error = null;
if (scheduler is null ||
!scheduler.TryCallGuestFunction(ctx, initRoutine, 0, 0, 0, 0, "pthread_once", out error))
{
lock (gate)
{
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
Monitor.PulseAll(gate);
}
TracePthreadOnce(onceAddress, initRoutine, "failed", error);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
lock (gate)
{
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceDone))
{
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
Monitor.PulseAll(gate);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Monitor.PulseAll(gate);
}
}
TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
{
if (mutexAddress == 0)
@@ -364,19 +477,13 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
lock (_stateGate)
{
_mutexStates[mutexAddress] = state;
_mutexStates[handle] = state;
}
_mutexStates[mutexAddress] = state;
_mutexStates[handle] = state;
if (!ctx.TryWriteUInt64(mutexAddress, handle))
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, handle))
{
lock (_stateGate)
{
_mutexStates.Remove(mutexAddress);
_mutexStates.Remove(handle);
}
_mutexStates.TryRemove(mutexAddress, out _);
_mutexStates.TryRemove(handle, out _);
state.Semaphore.Dispose();
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -393,15 +500,10 @@ public static class KernelPthreadCompatExports
}
var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress);
PthreadMutexState? state;
lock (_stateGate)
_mutexStates.TryRemove(resolvedAddress, out var state);
if (resolvedAddress != mutexAddress)
{
_mutexStates.TryGetValue(resolvedAddress, out state);
_mutexStates.Remove(resolvedAddress);
if (resolvedAddress != mutexAddress)
{
_mutexStates.Remove(mutexAddress);
}
_mutexStates.TryRemove(mutexAddress, out _);
}
if (state is null)
@@ -409,7 +511,7 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
_ = ctx.TryWriteUInt64(mutexAddress, 0);
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
state.Semaphore.Dispose();
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -439,7 +541,7 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp)
if (state.Type is MutexTypeDefault or MutexTypeNormal or MutexTypeAdaptiveNp)
{
if (tryOnly)
{
@@ -464,15 +566,33 @@ public static class KernelPthreadCompatExports
}
}
var acquired = true;
if (tryOnly)
var acquired = state.Semaphore.Wait(0);
if (!acquired)
{
acquired = state.Semaphore.Wait(0);
}
else
{
state.Semaphore.Wait();
// Guest-thread blocking for pthread_mutex_lock is currently disabled.
// Demon's Souls deadlocks during PS5SyncEvent initialization.
/* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
if (!tryOnly &&
GuestThreadExecution.IsGuestThread &&
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
GuestThreadExecution.RequestCurrentThreadBlock(
ctx,
"pthread_mutex_lock",
GetMutexWakeKey(resolvedAddress),
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
{
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} */
if (!tryOnly)
{
state.Semaphore.Wait();
acquired = true;
}
}
if (!acquired)
{
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
@@ -531,6 +651,7 @@ public static class KernelPthreadCompatExports
try
{
state.Semaphore.Release();
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
}
catch (SemaphoreFullException)
{
@@ -567,7 +688,7 @@ public static class KernelPthreadCompatExports
_mutexAttrStates[handle] = initialState;
}
if (!ctx.TryWriteUInt64(attrAddress, handle))
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, handle))
{
lock (_stateGate)
{
@@ -666,22 +787,16 @@ public static class KernelPthreadCompatExports
return 0;
}
lock (_stateGate)
if (_mutexStates.ContainsKey(mutexAddress))
{
if (_mutexStates.ContainsKey(mutexAddress))
{
return mutexAddress;
}
return mutexAddress;
}
if (ctx.TryReadUInt64(mutexAddress, out var pointedHandle) && pointedHandle != 0)
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle) && pointedHandle != 0)
{
lock (_stateGate)
if (_mutexStates.ContainsKey(pointedHandle))
{
if (_mutexStates.ContainsKey(pointedHandle))
{
return pointedHandle;
}
return pointedHandle;
}
}
@@ -697,16 +812,13 @@ public static class KernelPthreadCompatExports
return false;
}
lock (_stateGate)
if (_mutexStates.TryGetValue(mutexAddress, out state))
{
if (_mutexStates.TryGetValue(mutexAddress, out state))
{
resolvedAddress = mutexAddress;
return true;
}
resolvedAddress = mutexAddress;
return true;
}
if (!ctx.TryReadUInt64(mutexAddress, out var pointedHandle))
if (!KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle))
{
return false;
}
@@ -718,14 +830,11 @@ public static class KernelPthreadCompatExports
if (pointedHandle != 0)
{
lock (_stateGate)
if (_mutexStates.TryGetValue(pointedHandle, out state))
{
if (_mutexStates.TryGetValue(pointedHandle, out state))
{
_mutexStates[mutexAddress] = state;
resolvedAddress = pointedHandle;
return true;
}
_mutexStates.TryAdd(mutexAddress, state);
resolvedAddress = pointedHandle;
return true;
}
resolvedAddress = pointedHandle;
@@ -748,7 +857,7 @@ public static class KernelPthreadCompatExports
return 0;
}
if (ctx.TryReadUInt64(attrAddress, out var pointedHandle) && pointedHandle != 0)
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, attrAddress, out var pointedHandle) && pointedHandle != 0)
{
lock (_stateGate)
{
@@ -801,7 +910,7 @@ public static class KernelPthreadCompatExports
}
}
if (ctx.TryReadUInt64(condAddress, out var pointedHandle) && pointedHandle != 0)
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, condAddress, out var pointedHandle) && pointedHandle != 0)
{
lock (_stateGate)
{
@@ -833,7 +942,7 @@ public static class KernelPthreadCompatExports
}
}
if (ctx is null || !ctx.TryReadUInt64(condAddress, out var pointedHandle))
if (ctx is null || !KernelMemoryCompatExports.TryReadUInt64Compat(ctx, condAddress, out var pointedHandle))
{
return false;
}
@@ -872,7 +981,7 @@ public static class KernelPthreadCompatExports
_condStates[handle] = createdState;
}
if (!ctx.TryWriteUInt64(condAddress, handle))
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, handle))
{
lock (_stateGate)
{
@@ -936,7 +1045,7 @@ public static class KernelPthreadCompatExports
_condStates[handle] = state;
}
if (!ctx.TryWriteUInt64(condAddress, handle))
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, handle))
{
lock (_stateGate)
{
@@ -967,7 +1076,7 @@ public static class KernelPthreadCompatExports
}
}
_ = ctx.TryWriteUInt64(condAddress, 0);
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1103,6 +1212,63 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
$"pthread_mutex:0x{resolvedMutexAddress:X16}";
private static bool TryReserveBlockedMutexLock(
CpuContext ctx,
ulong mutexAddress,
ulong resolvedAddress,
PthreadMutexState state,
PthreadMutexWaiter waiter)
{
lock (state)
{
if (state.OwnerThreadId != 0 || state.RecursionCount != 0)
{
TracePthreadMutex(ctx, "lock-reserve-busy", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return false;
}
state.OwnerThreadId = waiter.ThreadId;
state.RecursionCount = 1;
Interlocked.Exchange(ref waiter.Reserved, 1);
}
TracePthreadMutex(ctx, "lock-reserve", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return true;
}
private static int CompleteBlockedMutexLock(
CpuContext ctx,
ulong mutexAddress,
ulong resolvedAddress,
PthreadMutexState state,
PthreadMutexWaiter waiter)
{
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
if (Interlocked.Exchange(ref waiter.Reserved, 0) == 1)
{
TracePthreadMutex(ctx, "lock-resume", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!state.Semaphore.Wait(0))
{
TracePthreadMutex(ctx, "lock-resume-busy", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
lock (state)
{
state.OwnerThreadId = currentThreadId;
state.RecursionCount = 1;
}
TracePthreadMutex(ctx, "lock-resume", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
{
if (timeoutUsec == 0)
@@ -1136,6 +1302,46 @@ public static class KernelPthreadCompatExports
};
}
private static object GetPthreadOnceGate(ulong onceAddress)
{
lock (_stateGate)
{
if (!_onceGates.TryGetValue(onceAddress, out var gate))
{
gate = new object();
_onceGates[onceAddress] = gate;
}
return gate;
}
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
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 TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
{
var createdState = new PthreadMutexState
@@ -1174,13 +1380,10 @@ public static class KernelPthreadCompatExports
_mutexStates[handle] = createdState;
}
if (!ctx.TryWriteUInt64(mutexAddress, handle))
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, handle))
{
lock (_stateGate)
{
_mutexStates.Remove(mutexAddress);
_mutexStates.Remove(handle);
}
_mutexStates.TryRemove(mutexAddress, out _);
_mutexStates.TryRemove(handle, out _);
resolvedAddress = 0;
state = null;
@@ -1204,15 +1407,27 @@ public static class KernelPthreadCompatExports
$"[LOADER][TRACE] pthread_self: stale_rdi=0x{ctx[CpuRegister.Rdi]:X16} thread=0x{currentThreadHandle:X16} tid=0x{currentThreadId:X16}");
}
private static void TracePthreadMutex(CpuContext ctx, string operation, ulong mutexAddress, ulong resolvedAddress, PthreadMutexState? state, ulong currentThreadId, int result)
private static void TracePthreadOnce(ulong onceAddress, ulong initRoutine, string operation, string? error)
{
if (!ShouldTracePthread())
{
return;
}
_ = ctx.TryReadUInt64(mutexAddress, out var guestWord0);
_ = ctx.TryReadUInt64(mutexAddress + 8, out var guestWord1);
var suffix = string.IsNullOrWhiteSpace(error) ? string.Empty : $" error={error}";
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_once_{operation}: once=0x{onceAddress:X16} init=0x{initRoutine:X16}{suffix}");
}
private static void TracePthreadMutex(CpuContext ctx, string operation, ulong mutexAddress, ulong resolvedAddress, PthreadMutexState? state, ulong currentThreadId, int result)
{
if (!ShouldTracePthreadMutex(mutexAddress, resolvedAddress))
{
return;
}
_ = KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var guestWord0);
_ = KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress + 8, out var guestWord1);
Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
@@ -1222,7 +1437,7 @@ public static class KernelPthreadCompatExports
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
{
if (!ShouldTracePthread())
if (!_tracePthreadConds)
{
return;
}
@@ -1234,6 +1449,45 @@ public static class KernelPthreadCompatExports
private static bool ShouldTracePthread()
{
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
return _tracePthreads;
}
private static bool ShouldTracePthreadMutex(ulong mutexAddress, ulong resolvedAddress)
{
if (_tracePthreadMutexFilter is null || _tracePthreadMutexFilter.Count == 0)
{
return _tracePthreads;
}
return _tracePthreadMutexFilter.Contains(mutexAddress) ||
_tracePthreadMutexFilter.Contains(resolvedAddress);
}
private static HashSet<ulong>? ParseTraceAddressFilter(string? filter)
{
if (string.IsNullOrWhiteSpace(filter))
{
return null;
}
var addresses = new HashSet<ulong>();
foreach (var token in filter.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
{
var normalized = token.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
? token[2..]
: token;
normalized = normalized.TrimStart('0');
if (ulong.TryParse(
normalized.Length == 0 ? "0" : normalized,
System.Globalization.NumberStyles.HexNumber,
System.Globalization.CultureInfo.InvariantCulture,
out var address))
{
addresses.Add(address);
}
}
return addresses.Count == 0 ? null : addresses;
}
}
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
@@ -26,12 +27,12 @@ public static class KernelPthreadExtendedCompatExports
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
private static readonly Dictionary<ulong, PthreadAttrState> _attrStates = new();
private static readonly Dictionary<ulong, PthreadRwlockState> _rwlockStates = new();
private static readonly Dictionary<int, TlsKeyState> _tlsKeys = new();
private static readonly ConcurrentDictionary<int, TlsKeyState> _tlsKeys = new();
private static int _nextTlsKey = 1;
private static long _nextSyntheticRwlockHandleId = 1;
private static long _nextSyntheticPthreadAttrHandleId = 1;
private static readonly Dictionary<ulong, Dictionary<int, ulong>> _threadLocalSpecific = new();
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
private sealed class ThreadState
{
@@ -185,6 +186,35 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "rcrVFJsQWRY",
ExportName = "scePthreadGetaffinity",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadGetaffinity(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var outMaskAddress = ctx[CpuRegister.Rsi];
if (thread == 0 || outMaskAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ulong affinityMask;
lock (_stateGate)
{
affinityMask = GetOrCreateThreadStateLocked(thread).AffinityMask;
}
if (!ctx.TryWriteUInt64(outMaskAddress, affinityMask))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "1tKyG7RlMJo",
ExportName = "scePthreadGetprio",
@@ -764,7 +794,7 @@ public static class KernelPthreadExtendedCompatExports
_rwlockStates[syntheticHandle] = rwlock;
}
_ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle);
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -818,7 +848,7 @@ public static class KernelPthreadExtendedCompatExports
}
}
_ = ctx.TryWriteUInt64(rwlockAddress, 0);
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -930,15 +960,13 @@ public static class KernelPthreadExtendedCompatExports
}
int key;
lock (_stateGate)
while (true)
{
while (_tlsKeys.ContainsKey(_nextTlsKey))
key = Interlocked.Increment(ref _nextTlsKey) - 1;
if (_tlsKeys.TryAdd(key, new TlsKeyState(destructor)))
{
_nextTlsKey++;
break;
}
key = _nextTlsKey++;
_tlsKeys[key] = new TlsKeyState(destructor);
}
if (!TryWriteInt32(ctx, outKeyAddress, key))
@@ -965,17 +993,14 @@ public static class KernelPthreadExtendedCompatExports
public static int PosixPthreadKeyDelete(CpuContext ctx)
{
var key = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_stateGate)
if (!_tlsKeys.TryRemove(key, out _))
{
if (!_tlsKeys.Remove(key))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
foreach (var entry in _threadLocalSpecific)
{
entry.Value.Remove(key);
}
foreach (var values in _threadLocalSpecific.Values)
{
values.TryRemove(key, out _);
}
ctx[CpuRegister.Rax] = 0;
@@ -999,22 +1024,15 @@ public static class KernelPthreadExtendedCompatExports
var key = unchecked((int)ctx[CpuRegister.Rdi]);
var value = ctx[CpuRegister.Rsi];
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
lock (_stateGate)
if (!_tlsKeys.ContainsKey(key))
{
if (!_tlsKeys.TryGetValue(key, out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (!_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values))
{
values = new Dictionary<int, ulong>();
_threadLocalSpecific[currentThreadHandle] = values;
}
values[key] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var values = _threadLocalSpecific.GetOrAdd(
currentThreadHandle,
static _ => new ConcurrentDictionary<int, ulong>());
values[key] = value;
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1036,19 +1054,16 @@ public static class KernelPthreadExtendedCompatExports
var key = unchecked((int)ctx[CpuRegister.Rdi]);
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
ulong value = 0;
lock (_stateGate)
if (!_tlsKeys.ContainsKey(key))
{
if (!_tlsKeys.TryGetValue(key, out _))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) &&
values.TryGetValue(key, out var storedValue))
{
value = storedValue;
}
if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) &&
values.TryGetValue(key, out var storedValue))
{
value = storedValue;
}
ctx[CpuRegister.Rax] = value;
@@ -1134,7 +1149,7 @@ public static class KernelPthreadExtendedCompatExports
}
}
if (ctx.TryReadUInt64(rwlockAddress, out var pointedHandle) && pointedHandle != 0)
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, rwlockAddress, out var pointedHandle) && pointedHandle != 0)
{
lock (_stateGate)
{
@@ -1166,7 +1181,7 @@ public static class KernelPthreadExtendedCompatExports
}
}
if (!ctx.TryReadUInt64(rwlockAddress, out var pointedHandle))
if (!KernelMemoryCompatExports.TryReadUInt64Compat(ctx, rwlockAddress, out var pointedHandle))
{
return false;
}
@@ -1201,7 +1216,7 @@ public static class KernelPthreadExtendedCompatExports
_rwlockStates[syntheticHandle] = createdRwlock;
}
_ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle);
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle);
resolvedAddress = syntheticHandle;
rwlock = createdRwlock;
return true;
+4 -10
View File
@@ -1,6 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
@@ -11,8 +12,7 @@ internal static class KernelPthreadState
{
private const int ThreadObjectSize = 0x1000;
private static readonly object Gate = new();
private static readonly Dictionary<ulong, ThreadIdentity> Threads = new();
private static readonly ConcurrentDictionary<ulong, ThreadIdentity> Threads = new();
private static readonly byte[] ZeroThreadObject = new byte[ThreadObjectSize];
private static long _nextUniqueThreadId = 1;
@@ -56,10 +56,7 @@ internal static class KernelPthreadState
internal static bool TryGetThreadIdentity(ulong threadHandle, out ThreadIdentity identity)
{
lock (Gate)
{
return Threads.TryGetValue(threadHandle, out identity);
}
return Threads.TryGetValue(threadHandle, out identity);
}
private static void EnsureCurrentThreadRegistered()
@@ -81,10 +78,7 @@ internal static class KernelPthreadState
Marshal.Copy(ZeroThreadObject, 0, pointer, ThreadObjectSize);
var handle = unchecked((ulong)pointer.ToInt64());
lock (Gate)
{
Threads[handle] = new ThreadIdentity(uniqueId, string.IsNullOrWhiteSpace(name) ? $"Thread-{uniqueId:X}" : name);
}
Threads[handle] = new ThreadIdentity(uniqueId, string.IsNullOrWhiteSpace(name) ? $"Thread-{uniqueId:X}" : name);
return handle;
}
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.Libs.Fiber;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Diagnostics;
@@ -32,6 +33,8 @@ public static class KernelRuntimeCompatExports
private const ulong DefaultKernelTscFrequency = 10_000_000UL;
private const ulong PrtAreaStartAddress = 0x0000001000000000UL;
private const ulong PrtAreaSize = 0x000000EC00000000UL;
private const int MapFlagFixed = 0x10;
private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
private const int AioInitParamSize = 0x3C;
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
@@ -54,6 +57,13 @@ public static class KernelRuntimeCompatExports
private static readonly (ulong Base, ulong Size)[] _prtApertures = new (ulong Base, ulong Size)[3];
private static int _stackChkFailCount;
private static long _usleepTraceCount;
private static readonly bool _traceUsleep =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal);
private static readonly bool _traceGuestThreads =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal);
[ThreadStatic]
private static int _shortUsleepCount;
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate ulong RdtscDelegate();
@@ -73,12 +83,24 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelUsleep");
if (micros < 1000)
{
Thread.Yield();
// Guest worker pools use usleep(1) as a polling backoff. Periodically
// relinquish a full host time slice so spin workers cannot starve producers.
if ((++_shortUsleepCount & 31) == 0)
{
Thread.Sleep(1);
}
else
{
Thread.Yield();
}
}
else
{
_shortUsleepCount = 0;
var sleepMilliseconds = (int)Math.Min((micros + 999UL) / 1000UL, int.MaxValue);
Thread.Sleep(sleepMilliseconds);
}
@@ -89,7 +111,7 @@ public static class KernelRuntimeCompatExports
private static void TraceUsleepSpin(CpuContext ctx, ulong micros)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal))
if (!_traceUsleep)
{
return;
}
@@ -101,6 +123,8 @@ public static class KernelRuntimeCompatExports
}
var rbx = ctx[CpuRegister.Rbx];
var r12 = ctx[CpuRegister.R12];
var r13 = ctx[CpuRegister.R13];
var lockAddress = rbx == 0 ? 0 : rbx + 0xF78;
var lockText = "unreadable";
if (lockAddress != 0 && ctx.TryReadUInt64(lockAddress, out var lockValue))
@@ -108,8 +132,46 @@ public static class KernelRuntimeCompatExports
lockText = $"0x{lockValue:X16}";
}
var schedulerText = "unreadable";
if (r12 != 0 && ctx.TryReadUInt64(r12 + 8, out var schedulerAddress))
{
schedulerText = $"0x{schedulerAddress:X16}";
}
var waitValueText = "unreadable";
if (r13 != 0 && ctx.TryReadUInt64(r13, out var waitValue))
{
waitValueText = $"0x{waitValue:X16}";
}
var callerReturnText = "unreadable";
var rbp = ctx[CpuRegister.Rbp];
if (rbp != 0 && ctx.TryReadUInt64(rbp + 8, out var callerReturn))
{
callerReturnText = $"0x{callerReturn:X16}";
}
var returnRip = GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)
? frame.ReturnRip
: 0UL;
var thread = GuestThreadExecution.CurrentGuestThreadHandle;
var fiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
Console.Error.WriteLine(
$"[LOADER][TRACE] usleep#{count}: usec={micros} rbx=0x{rbx:X16} lock@+F78=0x{lockAddress:X16}:{lockText} r13=0x{ctx[CpuRegister.R13]:X16} r14=0x{ctx[CpuRegister.R14]:X16} r15=0x{ctx[CpuRegister.R15]:X16}");
$"[LOADER][TRACE] usleep#{count}: usec={micros} ret=0x{returnRip:X16} caller={callerReturnText} thread=0x{thread:X16} fiber=0x{fiber:X16} rbx=0x{rbx:X16} lock@+F78=0x{lockAddress:X16}:{lockText} r12=0x{r12:X16} scheduler@+8={schedulerText} r13=0x{r13:X16}:{waitValueText} r14=0x{ctx[CpuRegister.R14]:X16} r15=0x{ctx[CpuRegister.R15]:X16}");
if (count % 100000 == 0 &&
_traceGuestThreads &&
GuestThreadExecution.Scheduler is { } scheduler)
{
foreach (var snapshot in scheduler.SnapshotThreads())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] guest_thread.snapshot handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
}
}
}
[SysAbiExport(
@@ -549,19 +611,16 @@ public static class KernelRuntimeCompatExports
public static int ErrorAddress(CpuContext ctx)
{
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
if (address != 0)
{
Span<byte> zero = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryWrite(address, zero))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
ctx[CpuRegister.Rax] = address;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
internal static bool TrySetErrno(CpuContext ctx, int value)
{
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
return address != 0 && TryWriteInt32(ctx, address, value);
}
[SysAbiExport(
Nid = "bnZxYgAFeA0",
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
@@ -653,7 +712,7 @@ public static class KernelRuntimeCompatExports
{
var inOutAddressPointer = ctx[CpuRegister.Rdi];
var length = ctx[CpuRegister.Rsi];
var _flags = ctx[CpuRegister.Rdx];
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
var alignment = ctx[CpuRegister.Rcx];
if (inOutAddressPointer == 0 || length == 0)
{
@@ -665,7 +724,8 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var effectiveAlignment = alignment == 0 ? 0x10000UL : alignment;
var effectiveAlignment = alignment == 0 ? DefaultVirtualRangeAlignment : alignment;
var fixedMapping = (flags & MapFlagFixed) != 0;
ulong desiredAddress;
lock (_stateGate)
{
@@ -674,13 +734,13 @@ public static class KernelRuntimeCompatExports
: AlignUp(_nextReservedVirtualBase, effectiveAlignment);
}
if (!TryReserveVirtualRange(ctx, desiredAddress, length, out var mappedAddress))
if (!TryReserveVirtualRange(ctx, desiredAddress, length, effectiveAlignment, allowSearch: !fixedMapping, out var mappedAddress))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16}");
$"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16} flags=0x{flags:X8} align=0x{effectiveAlignment:X16}");
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
{
@@ -964,14 +1024,12 @@ public static class KernelRuntimeCompatExports
public static int StackCheckFail(CpuContext ctx)
{
var count = Interlocked.Increment(ref _stackChkFailCount);
if (count <= 8)
{
Console.Error.WriteLine(
$"[LOADER][WARNING] __stack_chk_fail recovery#{count}: rip=0x{ctx.Rip:X16} rdi=0x{ctx[CpuRegister.Rdi]:X16}");
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
Console.Error.WriteLine(
$"[LOADER][ERROR] __stack_chk_fail#{count}: rip=0x{ctx.Rip:X16} rdi=0x{ctx[CpuRegister.Rdi]:X16}");
var result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP;
GuestThreadExecution.RequestCurrentEntryExit("__stack_chk_fail", result);
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
[SysAbiExport(
@@ -1474,30 +1532,40 @@ public static class KernelRuntimeCompatExports
ulong.TryParse(overrideHzText, out var overrideHz) &&
overrideHz >= minSane)
{
TraceKernelTscFrequency("env", overrideHz);
return overrideHz;
}
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
{
return cpuidHz;
}
if (TryCalibrateHostTscFrequency(out ulong calibratedHz) && calibratedHz >= minSane)
{
TraceKernelTscFrequency("calibrated-rdtsc", calibratedHz);
return calibratedHz;
}
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
{
TraceKernelTscFrequency("cpuid", cpuidHz);
return cpuidHz;
}
var hostQpc = Stopwatch.Frequency > 0
? unchecked((ulong)Stopwatch.Frequency)
: DefaultKernelTscFrequency;
if (hostQpc >= minSane)
{
TraceKernelTscFrequency("qpc", hostQpc);
return hostQpc;
}
TraceKernelTscFrequency("default", DefaultKernelTscFrequency);
return DefaultKernelTscFrequency;
}
private static void TraceKernelTscFrequency(string source, ulong frequencyHz)
{
Console.Error.WriteLine($"[LOADER][INFO] Kernel TSC frequency: {frequencyHz} Hz ({source})");
}
private static bool TryResolveCpuidTscFrequency(out ulong frequencyHz)
{
frequencyHz = 0;
@@ -1656,7 +1724,13 @@ public static class KernelRuntimeCompatExports
[DllImport("kernel32.dll", SetLastError = true)]
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
private static bool TryReserveVirtualRange(CpuContext ctx, ulong desiredAddress, ulong length, out ulong mappedAddress)
private static bool TryReserveVirtualRange(
CpuContext ctx,
ulong desiredAddress,
ulong length,
ulong alignment,
bool allowSearch,
out ulong mappedAddress)
{
mappedAddress = 0;
if (length == 0)
@@ -1668,40 +1742,51 @@ public static class KernelRuntimeCompatExports
{
object memoryObject = ctx.Memory;
MethodInfo? allocateAt = null;
MethodInfo? allocateAtOrAbove = null;
var allocateAtHasAllowAlternativeArg = false;
for (var depth = 0; depth < 4; depth++)
{
foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
{
if (!string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
{
continue;
}
var parameters = candidate.GetParameters();
if (parameters.Length == 3 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool))
{
allocateAt = candidate;
allocateAtHasAllowAlternativeArg = false;
break;
}
if (parameters.Length == 4 &&
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
parameters.Length == 5 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool) &&
parameters[3].ParameterType == typeof(bool))
parameters[3].ParameterType == typeof(ulong) &&
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
{
allocateAtOrAbove = candidate;
}
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
{
if (parameters.Length == 3 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool))
{
allocateAt = candidate;
allocateAtHasAllowAlternativeArg = false;
}
else if (parameters.Length == 4 &&
parameters[0].ParameterType == typeof(ulong) &&
parameters[1].ParameterType == typeof(ulong) &&
parameters[2].ParameterType == typeof(bool) &&
parameters[3].ParameterType == typeof(bool))
{
allocateAt = candidate;
allocateAtHasAllowAlternativeArg = true;
}
}
if (allocateAtOrAbove is not null && allocateAt is not null)
{
allocateAt = candidate;
allocateAtHasAllowAlternativeArg = true;
break;
}
}
if (allocateAt is not null)
if (allocateAtOrAbove is not null || allocateAt is not null)
{
break;
}
@@ -1721,6 +1806,18 @@ public static class KernelRuntimeCompatExports
memoryObject = innerValue;
}
if (allowSearch && allocateAtOrAbove is not null)
{
var searchArgs = new object[] { desiredAddress, length, false, alignment, 0UL };
var searchResult = allocateAtOrAbove.Invoke(memoryObject, searchArgs);
if (searchResult is bool trueValue && trueValue &&
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
{
mappedAddress = searchedAddress;
return true;
}
}
if (allocateAt is null)
{
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt missing on {ctx.Memory.GetType().FullName}");
@@ -1728,7 +1825,7 @@ public static class KernelRuntimeCompatExports
}
var invokeArgs = allocateAtHasAllowAlternativeArg
? new object[] { desiredAddress, length, false, true }
? new object[] { desiredAddress, length, false, allowSearch }
: new object[] { desiredAddress, length, false };
var result = allocateAt.Invoke(memoryObject, invokeArgs);
if (result is not ulong allocated || allocated == 0)
@@ -120,7 +120,7 @@ public static class KernelSemaphoreCompatExports
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
if (!GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitSema"))
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
+122
View File
@@ -0,0 +1,122 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Network;
public static class HttpExports
{
private const int HttpErrorInvalidId = unchecked((int)0x80431100);
private const int HttpErrorInvalidValue = unchecked((int)0x804311FE);
private static readonly ConcurrentDictionary<int, HttpContext> Contexts = new();
private static readonly ConcurrentDictionary<int, HttpTemplate> Templates = new();
private static int _nextContextId;
private static int _nextTemplateId = 0x1000;
private sealed record HttpContext(int NetMemoryId, int SslContextId, ulong PoolSize);
private sealed record HttpTemplate(int ContextId, ulong UserAgentAddress, int HttpVersion, bool AutoProxyConfig);
[SysAbiExport(
Nid = "A9cVMUtEp4Y",
ExportName = "sceHttpInit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceHttp")]
public static int HttpInit(CpuContext ctx)
{
var netMemoryId = unchecked((int)ctx[CpuRegister.Rdi]);
var sslContextId = unchecked((int)ctx[CpuRegister.Rsi]);
var poolSize = ctx[CpuRegister.Rdx];
if (poolSize == 0)
{
return SetReturn(ctx, HttpErrorInvalidValue);
}
var id = Interlocked.Increment(ref _nextContextId);
Contexts[id] = new HttpContext(netMemoryId, sslContextId, poolSize);
TraceHttp("init", id, unchecked((ulong)netMemoryId), unchecked((ulong)sslContextId), poolSize, 0);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "0gYjPTR-6cY",
ExportName = "sceHttpCreateTemplate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceHttp")]
public static int HttpCreateTemplate(CpuContext ctx)
{
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Contexts.ContainsKey(contextId))
{
return SetReturn(ctx, HttpErrorInvalidId);
}
var userAgentAddress = ctx[CpuRegister.Rsi];
var httpVersion = unchecked((int)ctx[CpuRegister.Rdx]);
var autoProxyConfig = ctx[CpuRegister.Rcx] != 0;
var id = Interlocked.Increment(ref _nextTemplateId);
Templates[id] = new HttpTemplate(contextId, userAgentAddress, httpVersion, autoProxyConfig);
TraceHttp("create_template", id, unchecked((ulong)contextId), userAgentAddress, unchecked((ulong)httpVersion), autoProxyConfig ? 1UL : 0UL);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "4I8vEpuEhZ8",
ExportName = "sceHttpDeleteTemplate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceHttp")]
public static int HttpDeleteTemplate(CpuContext ctx)
{
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
return Templates.TryRemove(templateId, out _)
? SetReturn(ctx, 0)
: SetReturn(ctx, HttpErrorInvalidId);
}
[SysAbiExport(
Nid = "Ik-KpLTlf7Q",
ExportName = "sceHttpTerm",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceHttp")]
public static int HttpTerm(CpuContext ctx)
{
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Contexts.TryRemove(contextId, out _))
{
return SetReturn(ctx, HttpErrorInvalidId);
}
foreach (var pair in Templates)
{
if (pair.Value.ContextId == contextId)
{
Templates.TryRemove(pair.Key, out _);
}
}
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_HTTP"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] http.{operation} id={id} arg0=0x{arg0:X16} arg1=0x{arg1:X16} arg2=0x{arg2:X16} arg3=0x{arg3:X16}");
}
}
+209
View File
@@ -2,11 +2,40 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Network;
public static class NetCtlExports
{
private const int MaxCallbacks = 8;
private const int NatInfoSize = 16;
private const int NetCtlErrorNoSpace = unchecked((int)0x80412103);
private const int NetCtlErrorInvalidAddress = unchecked((int)0x80412107);
private const int NetCtlErrorNotConnected = unchecked((int)0x80412108);
private const int NetCtlInfoDevice = 1;
private const int NetCtlInfoEtherAddress = 2;
private const int NetCtlInfoMtu = 3;
private const int NetCtlInfoLink = 4;
private const int NetCtlInfoIpConfig = 11;
private const int NetCtlInfoDhcpHostname = 12;
private const int NetCtlInfoPppoeAuthName = 13;
private const int NetCtlInfoIpAddress = 14;
private const int NetCtlInfoNetmask = 15;
private const int NetCtlInfoDefaultRoute = 16;
private const int NetCtlInfoPrimaryDns = 17;
private const int NetCtlInfoSecondaryDns = 18;
private const int NetCtlInfoHttpProxyConfig = 19;
private const int NetCtlInfoHttpProxyServer = 20;
private const int NetCtlInfoHttpProxyPort = 21;
private const int NetCtlDeviceWired = 0;
private const int NetCtlLinkDisconnected = 0;
private const int NetCtlIpConfigStatic = 0;
private static readonly object CallbackGate = new();
private static readonly CallbackRegistration[] Callbacks = new CallbackRegistration[MaxCallbacks];
private readonly record struct CallbackRegistration(ulong Function, ulong Argument);
[SysAbiExport(
Nid = "gky0+oaNM4k",
ExportName = "sceNetCtlInit",
@@ -17,4 +46,184 @@ public static class NetCtlExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "JO4yuTuMoKI",
ExportName = "sceNetCtlGetNatInfo",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlGetNatInfo(CpuContext ctx)
{
var natInfoAddress = ctx[CpuRegister.Rdi];
if (natInfoAddress == 0)
{
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
Span<byte> natInfo = stackalloc byte[NatInfoSize];
if (!ctx.Memory.TryRead(natInfoAddress, natInfo))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]);
if (size != NatInfoSize)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
BinaryPrimitives.WriteInt32LittleEndian(natInfo[4..], 1);
BinaryPrimitives.WriteInt32LittleEndian(natInfo[8..], 3);
BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001);
return ctx.Memory.TryWrite(natInfoAddress, natInfo)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "iQw3iQPhvUQ",
ExportName = "sceNetCtlCheckCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlCheckCallback(CpuContext ctx)
{
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "uBPlr0lbuiI",
ExportName = "sceNetCtlGetState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlGetState(CpuContext ctx)
{
var stateAddress = ctx[CpuRegister.Rdi];
if (stateAddress == 0)
{
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
Span<byte> stateBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0);
return ctx.Memory.TryWrite(stateAddress, stateBytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "UJ+Z7Q+4ck0",
ExportName = "sceNetCtlRegisterCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlRegisterCallback(CpuContext ctx)
{
var function = ctx[CpuRegister.Rdi];
var argument = ctx[CpuRegister.Rsi];
var callbackIdAddress = ctx[CpuRegister.Rdx];
if (function == 0 || callbackIdAddress == 0)
{
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
lock (CallbackGate)
{
var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0);
if (callbackId < 0)
{
return SetReturn(ctx, NetCtlErrorNoSpace);
}
Span<byte> callbackIdBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId));
if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Callbacks[callbackId] = new CallbackRegistration(function, argument);
}
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "obuxdTiwkF8",
ExportName = "sceNetCtlGetInfo",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNetCtl")]
public static int NetCtlGetInfo(CpuContext ctx)
{
var code = unchecked((int)ctx[CpuRegister.Rdi]);
var infoAddress = ctx[CpuRegister.Rsi];
if (infoAddress == 0)
{
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
return code switch
{
NetCtlInfoDevice => WriteUInt32(ctx, infoAddress, NetCtlDeviceWired),
NetCtlInfoEtherAddress => WriteZeroBytes(ctx, infoAddress, 6),
NetCtlInfoMtu => WriteUInt32(ctx, infoAddress, 1500),
NetCtlInfoLink => WriteUInt32(ctx, infoAddress, NetCtlLinkDisconnected),
NetCtlInfoIpConfig => WriteUInt32(ctx, infoAddress, NetCtlIpConfigStatic),
NetCtlInfoDhcpHostname => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
NetCtlInfoPppoeAuthName => WriteAsciiZ(ctx, infoAddress, string.Empty, 128),
NetCtlInfoIpAddress => WriteAsciiZ(ctx, infoAddress, "127.0.0.1", 16),
NetCtlInfoNetmask => WriteAsciiZ(ctx, infoAddress, "255.0.0.0", 16),
NetCtlInfoDefaultRoute => WriteAsciiZ(ctx, infoAddress, "127.0.0.1", 16),
NetCtlInfoPrimaryDns => WriteAsciiZ(ctx, infoAddress, "1.1.1.1", 16),
NetCtlInfoSecondaryDns => WriteAsciiZ(ctx, infoAddress, "1.1.1.1", 16),
NetCtlInfoHttpProxyConfig => WriteUInt32(ctx, infoAddress, 0),
NetCtlInfoHttpProxyServer => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
NetCtlInfoHttpProxyPort => WriteUInt16(ctx, infoAddress, 0),
_ => SetReturn(ctx, NetCtlErrorNotConnected),
};
}
private static int WriteZeroBytes(CpuContext ctx, ulong address, int count)
{
Span<byte> bytes = stackalloc byte[count];
return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int WriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int WriteUInt16(CpuContext ctx, ulong address, ushort value)
{
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int WriteAsciiZ(CpuContext ctx, ulong address, string value, int byteCount)
{
Span<byte> bytes = stackalloc byte[byteCount];
var copyCount = Math.Min(value.Length, byteCount - 1);
for (var i = 0; i < copyCount; i++)
{
bytes[i] = (byte)value[i];
}
return ctx.Memory.TryWrite(address, bytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
return result;
}
}
+70
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text;
using System.Threading;
@@ -16,11 +17,15 @@ public static class NetExports
private const int MaxNameLength = 256;
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
private static int _nextPoolId;
private static int _nextResolverId = 0x2000;
private static bool _initialized;
private sealed record NetPool(string Name, int Size, int Flags);
private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError);
[SysAbiExport(
Nid = "Nlev7Lg8k3A",
ExportName = "sceNetInit",
@@ -42,6 +47,7 @@ public static class NetExports
{
_initialized = false;
_pools.Clear();
_resolvers.Clear();
TraceNet("term", 0, 0, 0, 0);
return SetReturn(ctx, 0);
}
@@ -91,6 +97,70 @@ public static class NetExports
return SetReturn(ctx, 0);
}
[SysAbiExport(
Nid = "C4UgDHHPvdw",
ExportName = "sceNetResolverCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverCreate(CpuContext ctx)
{
var nameAddress = ctx[CpuRegister.Rdi];
var poolId = unchecked((int)ctx[CpuRegister.Rsi]);
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
if (flags != 0)
{
return SetReturn(ctx, NetErrorInvalidArgument);
}
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
? value
: string.Empty;
var id = Interlocked.Increment(ref _nextResolverId);
_resolvers[id] = new ResolverContext(name, poolId, flags, 0);
TraceNet("resolver.create", id, unchecked((ulong)poolId), unchecked((ulong)flags), _initialized ? 1UL : 0UL);
ctx[CpuRegister.Rax] = unchecked((ulong)id);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "kJlYH5uMAWI",
ExportName = "sceNetResolverDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverDestroy(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
return _resolvers.TryRemove(id, out _)
? SetReturn(ctx, 0)
: SetReturn(ctx, NetErrorBadFileDescriptor);
}
[SysAbiExport(
Nid = "J5i3hiLJMPk",
ExportName = "sceNetResolverGetError",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNet")]
public static int NetResolverGetError(CpuContext ctx)
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
var statusAddress = ctx[CpuRegister.Rsi];
if (statusAddress == 0)
{
return SetReturn(ctx, NetErrorInvalidArgument);
}
if (!_resolvers.TryGetValue(id, out var resolver))
{
return SetReturn(ctx, NetErrorBadFileDescriptor);
}
Span<byte> status = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
return ctx.Memory.TryWrite(statusAddress, status)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
+143
View File
@@ -0,0 +1,143 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Np;
public static class NpManagerExports
{
private const int NpTitleIdSize = 16;
private const int NpTitleSecretSize = 128;
[SysAbiExport(
Nid = "3Zl8BePTh9Y",
ExportName = "sceNpCheckCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpCheckCallback(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "JELHf4xPufo",
ExportName = "sceNpCheckCallbackForLib",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpCheckCallbackForLib(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "VfRSmPmj8Q8",
ExportName = "sceNpRegisterStateCallback",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpRegisterStateCallback(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "qQJfO8HAiaY",
ExportName = "sceNpRegisterStateCallbackA",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpRegisterStateCallbackA(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "0c7HbXRKUt4",
ExportName = "sceNpRegisterStateCallbackForToolkit",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManagerForToolkit")]
public static int NpRegisterStateCallbackForToolkit(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "eQH7nWPcAgc",
ExportName = "sceNpGetState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpGetState(CpuContext ctx)
{
var stateAddress = ctx[CpuRegister.Rsi];
if (stateAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> stateBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(stateBytes, 1);
return ctx.Memory.TryWrite(stateAddress, stateBytes)
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "Ec63y59l9tw",
ExportName = "sceNpSetNpTitleId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpManager")]
public static int NpSetNpTitleId(CpuContext ctx)
{
var titleIdAddress = ctx[CpuRegister.Rdi];
var titleSecretAddress = ctx[CpuRegister.Rsi];
if (titleIdAddress == 0 || titleSecretAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> titleId = stackalloc byte[NpTitleIdSize];
Span<byte> titleSecret = stackalloc byte[NpTitleSecretSize];
if (!ctx.Memory.TryRead(titleIdAddress, titleId) ||
!ctx.Memory.TryRead(titleSecretAddress, titleSecret))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceNp($"set_np_title_id title='{ReadTitleId(titleId)}'");
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 string ReadTitleId(ReadOnlySpan<byte> bytes)
{
var length = 0;
while (length < 12 && length < bytes.Length && bytes[length] != 0)
{
length++;
}
return length == 0
? string.Empty
: System.Text.Encoding.ASCII.GetString(bytes[..length]);
}
private static void TraceNp(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] np.{message}");
}
}
@@ -0,0 +1,20 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Np;
public static class NpSessionSignalingExports
{
[SysAbiExport(
Nid = "ysmw6J-P8Ak",
ExportName = "sceNpSessionSignalingInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpSessionSignaling")]
public static int NpSessionSignalingInitialize(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -0,0 +1,98 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.Np;
public static class NpUniversalDataSystemExports
{
private const int NpUniversalDataSystemErrorInvalidArgument = unchecked((int)0x80553102);
private static int _nextHandle = 1;
[SysAbiExport(
Nid = "sjaobBgqeB4",
ExportName = "sceNpUniversalDataSystemInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemInitialize(CpuContext ctx)
{
var parameterAddress = ctx[CpuRegister.Rdi];
if (parameterAddress == 0)
{
return SetReturn(ctx, NpUniversalDataSystemErrorInvalidArgument);
}
Span<byte> parameters = stackalloc byte[16];
return ctx.Memory.TryRead(parameterAddress, parameters)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "5zBnau1uIEo",
ExportName = "sceNpUniversalDataSystemCreateContext",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemCreateContext(CpuContext ctx)
{
var contextAddress = ctx[CpuRegister.Rdi];
if (contextAddress == 0)
{
return SetReturn(ctx, 0);
}
Span<byte> context = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(context, 1);
return ctx.Memory.TryWrite(contextAddress, context)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "hT0IAEvN+M0",
ExportName = "sceNpUniversalDataSystemCreateHandle",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemCreateHandle(CpuContext ctx)
{
var handle = Interlocked.Increment(ref _nextHandle);
if (TryWriteInt32(ctx, ctx[CpuRegister.Rdi], handle) ||
TryWriteInt32(ctx, ctx[CpuRegister.Rsi], handle))
{
return SetReturn(ctx, 0);
}
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "tpFJ8LIKvPw",
ExportName = "sceNpUniversalDataSystemRegisterContext",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemRegisterContext(CpuContext ctx)
{
return SetReturn(ctx, 0);
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
if (address == 0)
{
return false;
}
Span<byte> bytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
return result;
}
}
+33 -3
View File
@@ -128,6 +128,38 @@ public static class PadExports
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return WriteNeutralPadData(ctx, dataAddress)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "q1cHNfGycLI",
ExportName = "scePadRead",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadRead(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var dataAddress = ctx[CpuRegister.Rsi];
var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (handle != PrimaryPadHandle)
{
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
if (dataAddress == 0 || count < 1 || count > 64)
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return WriteNeutralPadData(ctx, dataAddress)
? SetReturn(ctx, 1)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
{
Span<byte> data = stackalloc byte[PadDataSize];
data.Clear();
data[0x04] = 128;
@@ -145,9 +177,7 @@ public static class PadExports
timestampMicroseconds);
data[0x68] = 1;
return ctx.Memory.TryWrite(dataAddress, data)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return ctx.Memory.TryWrite(dataAddress, data);
}
private static int SetReturn(CpuContext ctx, int result)
+587 -7
View File
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text.RegularExpressions;
namespace SharpEmu.Libs.PlayGo;
@@ -14,15 +15,36 @@ public static class PlayGoExports
private const int OrbisPlayGoErrorBadHandle = unchecked((int)0x80B20009);
private const int OrbisPlayGoErrorBadPointer = unchecked((int)0x80B2000A);
private const int OrbisPlayGoErrorBadSize = unchecked((int)0x80B2000B);
private const int OrbisPlayGoErrorBadChunkId = unchecked((int)0x80B2000C);
private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E);
private const int OrbisPlayGoErrorBadLocus = unchecked((int)0x80B20010);
private const ulong PlayGoInitBufAddrOffset = 0;
private const ulong PlayGoInitBufSizeOffset = 8;
private const uint PlayGoMinimumInitBufferSize = 0x200000;
private const uint PlayGoHandle = 1;
private const int PlayGoLocusNotDownloaded = 0;
private const int PlayGoLocusLocalSlow = 2;
private const int PlayGoLocusLocalFast = 3;
private const int PlayGoInstallSpeedSuspended = 0;
private const int PlayGoInstallSpeedTrickle = 1;
private const int PlayGoInstallSpeedFull = 2;
private const uint MaxPlayGoQueryEntries = 0x4000;
private static readonly Regex ChunkIdPattern = new(
@"<chunk\s+[^>]*\bid\s*=\s*""(?<id>\d+)""",
RegexOptions.CultureInvariant);
private static readonly Regex DefaultChunkPattern = new(
@"default_chunk\s*=\s*""(?<id>\d+)""",
RegexOptions.CultureInvariant);
private static readonly object _stateGate = new();
private static bool _initialized;
private static bool _hasPlayGoData;
private static bool _opened;
private static PlayGoMetadata _metadata = PlayGoMetadata.Empty;
private static int _installSpeed = PlayGoInstallSpeedTrickle;
private static ulong _languageMask = ulong.MaxValue;
private static int _unknownChunkDiagnostics;
[SysAbiExport(
Nid = "ts6GlZOKRrE",
@@ -67,7 +89,10 @@ public static class PlayGoExports
return OrbisPlayGoErrorAlreadyInitialized;
}
_hasPlayGoData = HasPlayGoChunkData();
_metadata = LoadPlayGoMetadata();
_installSpeed = PlayGoInstallSpeedTrickle;
_languageMask = ulong.MaxValue;
_opened = false;
_initialized = true;
}
@@ -100,10 +125,12 @@ public static class PlayGoExports
return OrbisPlayGoErrorNotInitialized;
}
if (!_hasPlayGoData)
if (!_metadata.Available)
{
return OrbisPlayGoErrorNotSupportPlayGo;
}
_opened = true;
}
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
@@ -132,7 +159,8 @@ public static class PlayGoExports
}
_initialized = false;
_hasPlayGoData = false;
_opened = false;
_metadata = PlayGoMetadata.Empty;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -157,20 +185,572 @@ public static class PlayGoExports
{
return OrbisPlayGoErrorBadHandle;
}
_opened = false;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool HasPlayGoChunkData()
[SysAbiExport(
Nid = "73fF1MFU8hA",
ExportName = "scePlayGoGetChunkId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetChunkId(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var outChunkIdList = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var outEntries = ctx[CpuRegister.Rcx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (outEntries == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (outChunkIdList != 0 && numberOfEntries == 0)
{
return OrbisPlayGoErrorBadSize;
}
ushort[] chunkIds;
lock (_stateGate)
{
chunkIds = _metadata.ChunkIds;
}
var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length;
if (outChunkIdList == 0)
{
return TryWriteUInt32(ctx, outEntries, availableEntries)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var entriesToWrite = Math.Min(numberOfEntries, availableEntries);
if (entriesToWrite > MaxPlayGoQueryEntries)
{
return OrbisPlayGoErrorBadSize;
}
for (uint i = 0; i < entriesToWrite; i++)
{
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
if (!TryWriteUInt16(ctx, outChunkIdList + (i * sizeof(ushort)), chunkId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
return TryWriteUInt32(ctx, outEntries, entriesToWrite)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "v6EZ-YWRdMs",
ExportName = "scePlayGoGetEta",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetEta(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var chunkIds = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var outEta = ctx[CpuRegister.Rcx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (chunkIds == 0 || outEta == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{
return OrbisPlayGoErrorBadSize;
}
return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0
? chunkError
: TryWriteInt64(ctx, outEta, 0)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "rvBSfTimejE",
ExportName = "scePlayGoGetInstallSpeed",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetInstallSpeed(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var outSpeed = ctx[CpuRegister.Rsi];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (outSpeed == 0)
{
return OrbisPlayGoErrorBadPointer;
}
int speed;
lock (_stateGate)
{
speed = _installSpeed;
}
return TryWriteInt32(ctx, outSpeed, speed)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "3OMbYZBaa50",
ExportName = "scePlayGoGetLanguageMask",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetLanguageMask(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var outLanguageMask = ctx[CpuRegister.Rsi];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (outLanguageMask == 0)
{
return OrbisPlayGoErrorBadPointer;
}
ulong languageMask;
lock (_stateGate)
{
languageMask = _languageMask;
}
return ctx.TryWriteUInt64(outLanguageMask, languageMask)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "uWIYLFkkwqk",
ExportName = "scePlayGoGetLocus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetLocus(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var chunkIds = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var outLoci = ctx[CpuRegister.Rcx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (chunkIds == 0 || outLoci == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{
return OrbisPlayGoErrorBadSize;
}
var loci = new byte[numberOfEntries];
for (uint i = 0; i < numberOfEntries; i++)
{
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!IsKnownChunkId(chunkId))
{
if (Interlocked.Increment(ref _unknownChunkDiagnostics) <= 8)
{
ushort[] knownChunkIds;
lock (_stateGate)
{
knownChunkIds = _metadata.ChunkIds;
}
Console.Error.WriteLine(
$"[LOADER][TRACE] playgo.unknown_chunk_id id={chunkId} entries={numberOfEntries} " +
$"known=[{string.Join(',', knownChunkIds)}]");
}
}
loci[i] = PlayGoLocusLocalFast;
}
return ctx.Memory.TryWrite(outLoci, loci)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "-RJWNMK3fC8",
ExportName = "scePlayGoGetProgress",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetProgress(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var chunkIds = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var outProgress = ctx[CpuRegister.Rcx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (chunkIds == 0 || outProgress == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{
return OrbisPlayGoErrorBadSize;
}
var chunkError = ValidateChunkIds(ctx, chunkIds, numberOfEntries);
if (chunkError != 0)
{
return chunkError;
}
Span<byte> progress = stackalloc byte[sizeof(ulong) * 2];
BinaryPrimitives.WriteUInt64LittleEndian(progress, 0);
BinaryPrimitives.WriteUInt64LittleEndian(progress[sizeof(ulong)..], 0);
return ctx.Memory.TryWrite(outProgress, progress)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "Nn7zKwnA5q0",
ExportName = "scePlayGoGetToDoList",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoGetToDoList(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var outTodoList = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var outEntries = ctx[CpuRegister.Rcx];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (outTodoList == 0 || outEntries == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (numberOfEntries == 0)
{
return OrbisPlayGoErrorBadSize;
}
return TryWriteUInt32(ctx, outEntries, 0)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "-Q1-u1a7p0g",
ExportName = "scePlayGoPrefetch",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoPrefetch(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var chunkIds = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var minimumLocus = unchecked((int)ctx[CpuRegister.Rcx]);
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (chunkIds == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{
return OrbisPlayGoErrorBadSize;
}
if (minimumLocus is not PlayGoLocusNotDownloaded and not PlayGoLocusLocalSlow and not PlayGoLocusLocalFast)
{
return OrbisPlayGoErrorBadLocus;
}
return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0
? chunkError
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "4AAcTU9R3XM",
ExportName = "scePlayGoSetInstallSpeed",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoSetInstallSpeed(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var speed = unchecked((int)ctx[CpuRegister.Rsi]);
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (speed is not PlayGoInstallSpeedSuspended and not PlayGoInstallSpeedTrickle and not PlayGoInstallSpeedFull)
{
return OrbisPlayGoErrorInvalidArgument;
}
lock (_stateGate)
{
_installSpeed = speed;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "LosLlHOpNqQ",
ExportName = "scePlayGoSetLanguageMask",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoSetLanguageMask(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var languageMask = ctx[CpuRegister.Rsi];
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
lock (_stateGate)
{
_languageMask = languageMask;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "gUPGiOQ1tmQ",
ExportName = "scePlayGoSetToDoList",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoSetToDoList(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
var todoList = ctx[CpuRegister.Rsi];
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
var validation = ValidateHandle(handle);
if (validation != 0)
{
return validation;
}
if (todoList == 0)
{
return OrbisPlayGoErrorBadPointer;
}
return numberOfEntries == 0
? OrbisPlayGoErrorBadSize
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int ValidateHandle(uint handle)
{
lock (_stateGate)
{
if (!_initialized)
{
return OrbisPlayGoErrorNotInitialized;
}
if (handle != PlayGoHandle || !_opened)
{
return OrbisPlayGoErrorBadHandle;
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static int ValidateChunkIds(CpuContext ctx, ulong chunkIds, uint numberOfEntries)
{
for (uint i = 0; i < numberOfEntries; i++)
{
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
_ = chunkId;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool IsKnownChunkId(ushort chunkId)
{
lock (_stateGate)
{
return _metadata.ChunkIds.Length == 0 || Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
}
}
private static PlayGoMetadata LoadPlayGoMetadata()
{
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
if (string.IsNullOrWhiteSpace(app0Root))
{
return PlayGoMetadata.Empty;
}
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
var scenarioJson = Path.Combine(app0Root, "sce_sys", "playgo-scenario.json");
var chunkDefsXml = Path.Combine(app0Root, "playgo-chunkdefs.xml");
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
if (!hasMetadata)
{
return PlayGoMetadata.Empty;
}
var chunkIds = LoadChunkIds(chunkDefsXml);
return new PlayGoMetadata(true, chunkIds);
}
private static ushort[] LoadChunkIds(string chunkDefsXml)
{
if (!File.Exists(chunkDefsXml))
{
return Array.Empty<ushort>();
}
try
{
var xml = File.ReadAllText(chunkDefsXml);
var chunkIds = new HashSet<ushort>();
AddChunkIds(xml, DefaultChunkPattern, chunkIds);
AddChunkIds(xml, ChunkIdPattern, chunkIds);
var sorted = chunkIds.ToArray();
Array.Sort(sorted);
return sorted;
}
catch (IOException)
{
return Array.Empty<ushort>();
}
catch (UnauthorizedAccessException)
{
return Array.Empty<ushort>();
}
}
private static void AddChunkIds(string xml, Regex pattern, HashSet<ushort> chunkIds)
{
foreach (Match match in pattern.Matches(xml))
{
if (ushort.TryParse(match.Groups["id"].Value, out var chunkId))
{
chunkIds.Add(chunkId);
}
}
}
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
{
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
var hostPath = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
return File.Exists(hostPath);
value = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
{
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> buffer = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryWriteInt64(CpuContext ctx, ulong address, long value)
{
Span<byte> buffer = stackalloc byte[sizeof(long)];
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private sealed record PlayGoMetadata(bool Available, ushort[] ChunkIds)
{
public static readonly PlayGoMetadata Empty = new(false, Array.Empty<ushort>());
}
}
+85 -2
View File
@@ -8,6 +8,8 @@ namespace SharpEmu.Libs.Rtc;
public static class RtcExports
{
private const long DateTimeTicksPerMicrosecond = 10;
[SysAbiExport(
Nid = "ZPD1YOKI+Kw",
ExportName = "sceRtcGetCurrentClockLocalTime",
@@ -42,6 +44,29 @@ public static class RtcExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "18B2NS1y9UU",
ExportName = "sceRtcGetCurrentTick",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcGetCurrentTick(CpuContext ctx)
{
var tickAddress = ctx[CpuRegister.Rdi];
if (tickAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "8w-H19ip48I",
ExportName = "sceRtcGetTick",
@@ -72,14 +97,57 @@ public static class RtcExports
rtcDateTime.Minute,
rtcDateTime.Second,
DateTimeKind.Utc);
tickValue = checked((ulong)((baseDateTime.Ticks / 10) + rtcDateTime.Microsecond));
tickValue = checked((ulong)((baseDateTime.Ticks / DateTimeTicksPerMicrosecond) + rtcDateTime.Microsecond));
}
catch (Exception ex) when (ex is ArgumentOutOfRangeException or OverflowException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "ueega6v3GUw",
ExportName = "sceRtcSetTick",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceRtc")]
public static int RtcSetTick(CpuContext ctx)
{
var dateTimeAddress = ctx[CpuRegister.Rdi];
var tickAddress = ctx[CpuRegister.Rsi];
if (dateTimeAddress == 0 || tickAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadUInt64(tickAddress, out var tickValue))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (tickValue > long.MaxValue / DateTimeTicksPerMicrosecond)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
DateTime dateTime;
try
{
dateTime = new DateTime(checked((long)tickValue * DateTimeTicksPerMicrosecond), DateTimeKind.Utc);
}
catch (ArgumentOutOfRangeException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
if (!TryWriteRtcDateTime(ctx, dateTimeAddress, dateTime))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -108,6 +176,21 @@ public static class RtcExports
return true;
}
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, DateTime dateTime)
{
Span<byte> rtcDateTime = stackalloc byte[16];
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)dateTime.Year));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)dateTime.Month));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)dateTime.Day));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)dateTime.Hour));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)dateTime.Minute));
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)dateTime.Second));
BinaryPrimitives.WriteUInt32LittleEndian(
rtcDateTime[12..16],
checked((uint)((dateTime.Ticks % TimeSpan.TicksPerSecond) / DateTimeTicksPerMicrosecond)));
return ctx.Memory.TryWrite(address, rtcDateTime);
}
private readonly record struct RtcDateTime(
ushort Year,
ushort Month,
+112
View File
@@ -0,0 +1,112 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Share;
public static class ShareExports
{
private const int MaxContentParamBytes = 4096;
private static int _initialized;
private static string _contentParam = string.Empty;
[SysAbiExport(
Nid = "nBDD66kiFW8",
ExportName = "sceShareInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceShareUtility")]
public static int ShareInitialize(CpuContext ctx)
{
var memorySize = ctx[CpuRegister.Rdi];
var priority = unchecked((int)ctx[CpuRegister.Rsi]);
var affinityMask = ctx[CpuRegister.Rdx];
if (memorySize == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Interlocked.Exchange(ref _initialized, 1);
TraceShare($"initialize memory=0x{memorySize:X} priority={priority} affinity=0x{affinityMask:X}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "7QZtURYnXG4",
ExportName = "sceShareSetContentParam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceShareUtility")]
public static int ShareSetContentParam(CpuContext ctx)
{
var contentParamAddress = ctx[CpuRegister.Rdi];
if (contentParamAddress == 0)
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!TryReadNullTerminatedUtf8(ctx, contentParamAddress, MaxContentParamBytes, out var contentParam))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
_contentParam = contentParam;
if (Volatile.Read(ref _initialized) == 0)
{
TraceShare("set_content_param before initialize");
}
TraceShare($"set_content_param len={contentParam.Length} preview='{FormatTraceString(contentParam)}'");
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 bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
{
Span<byte> bytes = stackalloc byte[maxLength];
Span<byte> one = stackalloc byte[1];
for (var index = 0; index < maxLength; index++)
{
if (!ctx.Memory.TryRead(address + (ulong)index, one))
{
value = string.Empty;
return false;
}
if (one[0] == 0)
{
value = Encoding.UTF8.GetString(bytes[..index]);
return true;
}
bytes[index] = one[0];
}
value = string.Empty;
return false;
}
private static string FormatTraceString(string value)
{
var normalized = value.Replace("\r", "\\r", StringComparison.Ordinal).Replace("\n", "\\n", StringComparison.Ordinal);
return normalized.Length <= 120 ? normalized : string.Concat(normalized.AsSpan(0, 120), "...");
}
private static void TraceShare(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SHARE"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] share.{message}");
}
}
@@ -0,0 +1,75 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.SystemGesture;
public static class SystemGestureExports
{
[SysAbiExport(
Nid = "3pcAvmwKCvM",
ExportName = "sceSystemGestureInitializePrimitiveTouchRecognizer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemGesture")]
public static int SystemGestureInitializePrimitiveTouchRecognizer(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "FWF8zkhr854",
ExportName = "sceSystemGestureCreateTouchRecognizer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemGesture")]
public static int SystemGestureCreateTouchRecognizer(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "qpo-mEOwje0",
ExportName = "sceSystemGestureOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemGesture")]
public static int SystemGestureOpen(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "GgFMb22sbbI",
ExportName = "sceSystemGestureUpdatePrimitiveTouchRecognizer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemGesture")]
public static int SystemGestureUpdatePrimitiveTouchRecognizer(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "j4h82CQWENo",
ExportName = "sceSystemGestureUpdateTouchRecognizer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemGesture")]
public static int SystemGestureUpdateTouchRecognizer(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "h8uongcBNVs",
ExportName = "sceSystemGestureGetTouchEventsCount",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemGesture")]
public static int SystemGestureGetTouchEventsCount(CpuContext ctx)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
}
@@ -12,6 +12,34 @@ public static class SystemServiceExports
private const int SystemServiceStatusSize = 0x0C;
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128;
[SysAbiExport(
Nid = "fZo48un7LK4",
ExportName = "sceSystemServiceParamGetInt",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemService")]
public static int SystemServiceParamGetInt(CpuContext ctx)
{
var parameterId = unchecked((int)ctx[CpuRegister.Rdi]);
var valueAddress = ctx[CpuRegister.Rsi];
if (valueAddress == 0)
{
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
}
var value = parameterId switch
{
1 or 2 or 3 or 1000 => 1,
4 => 180,
_ => 0,
};
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
return ctx.Memory.TryWrite(valueAddress, valueBytes)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "rPo6tV8D9bM",
ExportName = "sceSystemServiceGetStatus",
@@ -3,14 +3,21 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
using System.Threading;
namespace SharpEmu.Libs.UserService;
public static class UserServiceExports
{
private const int OrbisUserServiceErrorInvalidArgument = unchecked((int)0x80960005);
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
private const int PrimaryUserId = 1;
private const int InvalidUserId = -1;
private const string PrimaryUserName = "SharpEmu";
private static int _loginEventDelivered;
[SysAbiExport(
Nid = "j3YMu1MVNNo",
@@ -64,6 +71,89 @@ public static class UserServiceExports
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "yH17Q6NWtVg",
ExportName = "sceUserServiceGetEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetEvent(CpuContext ctx)
{
var eventAddress = ctx[CpuRegister.Rdi];
if (eventAddress == 0)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
{
return SetReturn(ctx, OrbisUserServiceErrorNoEvent);
}
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
return ctx.Memory.TryWrite(eventAddress, payload)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "1xxcMiGu2fo",
ExportName = "sceUserServiceGetUserName",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetUserName(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var nameAddress = ctx[CpuRegister.Rsi];
var capacity = ctx[CpuRegister.Rdx];
if (userId != PrimaryUserId)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
if (nameAddress == 0)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
if (capacity <= (ulong)nameBytes.Length)
{
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort);
}
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
nameBytes.CopyTo(output);
return ctx.Memory.TryWrite(nameAddress, output)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "D-CzAxQL0XI",
ExportName = "sceUserServiceGetPlatformPrivacySetting",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceUserService")]
public static int UserServiceGetPlatformPrivacySetting(CpuContext ctx)
{
var parameterId = unchecked((int)ctx[CpuRegister.Rdi]);
var valueAddress = ctx[CpuRegister.Rsi];
if (parameterId != 1000)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
if (valueAddress == 0)
{
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
return TryWriteInt32(ctx, valueAddress, 0)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
@@ -0,0 +1,213 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.IO.Compression;
namespace SharpEmu.Libs.VideoOut;
internal static class PngSplashLoader
{
private static ReadOnlySpan<byte> PngSignature =>
[
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
];
public static bool TryLoad(out byte[] pixels, out uint width, out uint height)
{
pixels = [];
width = 0;
height = 0;
try
{
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
if (string.IsNullOrWhiteSpace(app0Root))
{
return false;
}
var path = Path.Combine(app0Root, "sce_sys", "pic0.png");
if (!File.Exists(path))
{
return false;
}
return TryDecode(File.ReadAllBytes(path), out pixels, out width, out height);
}
catch
{
pixels = [];
width = 0;
height = 0;
return false;
}
}
private static bool TryDecode(
ReadOnlySpan<byte> png,
out byte[] pixels,
out uint width,
out uint height)
{
pixels = [];
width = 0;
height = 0;
if (png.Length < 33 || !png[..8].SequenceEqual(PngSignature))
{
return false;
}
byte bitDepth = 0;
byte colorType = 0;
byte interlace = 0;
using var compressed = new MemoryStream();
var offset = 8;
while (offset <= png.Length - 12)
{
var chunkLength = BinaryPrimitives.ReadUInt32BigEndian(png.Slice(offset, 4));
if (chunkLength > int.MaxValue || offset > png.Length - 12 - (int)chunkLength)
{
return false;
}
var chunkType = png.Slice(offset + 4, 4);
var chunkData = png.Slice(offset + 8, (int)chunkLength);
if (chunkType.SequenceEqual("IHDR"u8))
{
if (chunkData.Length != 13)
{
return false;
}
width = BinaryPrimitives.ReadUInt32BigEndian(chunkData[..4]);
height = BinaryPrimitives.ReadUInt32BigEndian(chunkData.Slice(4, 4));
bitDepth = chunkData[8];
colorType = chunkData[9];
interlace = chunkData[12];
}
else if (chunkType.SequenceEqual("IDAT"u8))
{
compressed.Write(chunkData);
}
else if (chunkType.SequenceEqual("IEND"u8))
{
break;
}
offset += checked((int)chunkLength + 12);
}
var sourceBytesPerPixel = colorType switch
{
2 => 3,
6 => 4,
_ => 0,
};
if (width == 0 ||
height == 0 ||
width > 16384 ||
height > 16384 ||
bitDepth != 8 ||
interlace != 0 ||
sourceBytesPerPixel == 0 ||
compressed.Length == 0)
{
return false;
}
var stride = checked((int)width * sourceBytesPerPixel);
var scanlineLength = checked(stride + 1);
var decompressedLength = checked(scanlineLength * (int)height);
var scanlines = GC.AllocateUninitializedArray<byte>(decompressedLength);
compressed.Position = 0;
using (var zlib = new ZLibStream(compressed, CompressionMode.Decompress))
{
zlib.ReadExactly(scanlines);
if (zlib.ReadByte() != -1)
{
return false;
}
}
var reconstructed = GC.AllocateUninitializedArray<byte>(checked(stride * (int)height));
for (var y = 0; y < (int)height; y++)
{
var sourceLine = scanlines.AsSpan(y * scanlineLength + 1, stride);
var targetLine = reconstructed.AsSpan(y * stride, stride);
var previousLine = y == 0
? ReadOnlySpan<byte>.Empty
: reconstructed.AsSpan((y - 1) * stride, stride);
if (!TryUnfilter(
scanlines[y * scanlineLength],
sourceLine,
previousLine,
targetLine,
sourceBytesPerPixel))
{
return false;
}
}
pixels = GC.AllocateUninitializedArray<byte>(checked((int)width * (int)height * 4));
for (int sourceOffset = 0, targetOffset = 0;
sourceOffset < reconstructed.Length;
sourceOffset += sourceBytesPerPixel, targetOffset += 4)
{
pixels[targetOffset] = reconstructed[sourceOffset + 2];
pixels[targetOffset + 1] = reconstructed[sourceOffset + 1];
pixels[targetOffset + 2] = reconstructed[sourceOffset];
pixels[targetOffset + 3] = sourceBytesPerPixel == 4
? reconstructed[sourceOffset + 3]
: (byte)0xFF;
}
return true;
}
private static bool TryUnfilter(
byte filter,
ReadOnlySpan<byte> source,
ReadOnlySpan<byte> previous,
Span<byte> target,
int bytesPerPixel)
{
for (var x = 0; x < source.Length; x++)
{
var left = x >= bytesPerPixel ? target[x - bytesPerPixel] : (byte)0;
var above = previous.IsEmpty ? (byte)0 : previous[x];
var upperLeft = !previous.IsEmpty && x >= bytesPerPixel
? previous[x - bytesPerPixel]
: (byte)0;
target[x] = filter switch
{
0 => source[x],
1 => unchecked((byte)(source[x] + left)),
2 => unchecked((byte)(source[x] + above)),
3 => unchecked((byte)(source[x] + ((left + above) >> 1))),
4 => unchecked((byte)(source[x] + Paeth(left, above, upperLeft))),
_ => source[x],
};
if (filter > 4)
{
return false;
}
}
return true;
}
private static byte Paeth(byte left, byte above, byte upperLeft)
{
var estimate = left + above - upperLeft;
var leftDistance = Math.Abs(estimate - left);
var aboveDistance = Math.Abs(estimate - above);
var upperLeftDistance = Math.Abs(estimate - upperLeft);
return leftDistance <= aboveDistance && leftDistance <= upperLeftDistance
? left
: aboveDistance <= upperLeftDistance
? above
: upperLeft;
}
}
+100 -1
View File
@@ -29,8 +29,11 @@ public static class VideoOutExports
private const int VideoOutBufferAttributeSize = 0x28;
private const int VideoOutBufferAttribute2Size = 0x50;
private const int VideoOutBuffersEntrySize = 0x20;
private const int VideoOutOutputStatusSize = 0x30;
private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
private const ulong SceVideoOutPixelFormatB8G8R8A8Unorm = 0x8100000000000000;
private const ulong SceVideoOutPixelFormatR8G8B8A8Unorm = 0x8100000022000000;
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
@@ -82,6 +85,10 @@ public static class VideoOutExports
public ulong VblankCount { get; set; }
public ulong FlipCount { get; set; }
public int CurrentBuffer { get; set; } = -1;
public uint OutputWidth { get; set; } = 1920;
public uint OutputHeight { get; set; } = 1080;
public uint RefreshRate { get; set; } = 60;
public float Gamma { get; set; } = 1.0f;
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
public List<FlipEventRegistration> FlipEvents { get; } = new();
@@ -196,6 +203,93 @@ public static class VideoOutExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "utPrVdxio-8",
ExportName = "sceVideoOutGetOutputStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutGetOutputStatus(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var statusAddress = ctx[CpuRegister.Rsi];
if (statusAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
Span<byte> status = stackalloc byte[VideoOutOutputStatusSize];
status.Clear();
var resolutionClass = port.OutputWidth >= 3840 || port.OutputHeight >= 2160 ? 2 : 1;
BinaryPrimitives.WriteInt32LittleEndian(status[0x00..0x04], resolutionClass);
BinaryPrimitives.WriteInt32LittleEndian(status[0x04..0x08], 1);
BinaryPrimitives.WriteUInt64LittleEndian(status[0x08..0x10], port.RefreshRate);
return ctx.Memory.TryWrite(statusAddress, status)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "DYhhWbJSeRg",
ExportName = "sceVideoOutColorSettingsSetGamma_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutColorSettingsSetGamma(CpuContext ctx)
{
var settingsAddress = ctx[CpuRegister.Rdi];
if (settingsAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
ctx.GetXmmRegister(0, out var xmm0Low, out _);
var gamma = BitConverter.Int32BitsToSingle(unchecked((int)xmm0Low));
if (!float.IsFinite(gamma) || gamma is < 0.1f or > 2.0f)
{
return OrbisVideoOutErrorInvalidValue;
}
Span<byte> gammaBytes = stackalloc byte[sizeof(float)];
BinaryPrimitives.WriteInt32LittleEndian(gammaBytes, BitConverter.SingleToInt32Bits(gamma));
return ctx.Memory.TryWrite(settingsAddress, gammaBytes)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
[SysAbiExport(
Nid = "pv9CI5VC+R0",
ExportName = "sceVideoOutAdjustColor_",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutAdjustColor(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var settingsAddress = ctx[CpuRegister.Rsi];
if (settingsAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
Span<byte> gammaBytes = stackalloc byte[sizeof(float)];
if (!ctx.Memory.TryRead(settingsAddress, gammaBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
port.Gamma = BitConverter.Int32BitsToSingle(
BinaryPrimitives.ReadInt32LittleEndian(gammaBytes));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "j6RaAUlaLv0",
ExportName = "sceVideoOutWaitVblank",
@@ -715,6 +809,8 @@ public static class VideoOutExports
Index = groupIndex,
Attribute = attribute,
};
port.OutputWidth = attribute.Width;
port.OutputHeight = attribute.Height;
for (var i = 0; i < addresses.Length; i++)
{
@@ -726,6 +822,7 @@ public static class VideoOutExports
TraceVideoOut(
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
return groupIndex;
}
}
@@ -939,6 +1036,8 @@ public static class VideoOutExports
private static uint GetBytesPerPixel(ulong pixelFormat) =>
pixelFormat is SceVideoOutPixelFormatA8R8G8B8Srgb or
SceVideoOutPixelFormatA8B8G8R8Srgb or
SceVideoOutPixelFormatB8G8R8A8Unorm or
SceVideoOutPixelFormatR8G8B8A8Unorm or
SceVideoOutPixelFormatA2R10G10B10 or
SceVideoOutPixelFormatA2R10G10B10Srgb or
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq
@@ -973,7 +1072,7 @@ public static class VideoOutExports
var dst = 0;
for (var src = 0; src + 3 < source.Length; src += 4)
{
if (pixelFormat == SceVideoOutPixelFormatA8B8G8R8Srgb)
if (pixelFormat is SceVideoOutPixelFormatA8B8G8R8Srgb or SceVideoOutPixelFormatR8G8B8A8Unorm)
{
destination[dst++] = source[src + 0];
destination[dst++] = source[src + 1];
@@ -23,8 +23,62 @@ internal static unsafe class VulkanVideoPresenter
private static readonly object _gate = new();
private static Thread? _thread;
private static Presentation? _latestPresentation;
private static uint _windowWidth;
private static uint _windowHeight;
private static bool _closed;
public static void EnsureStarted(uint width, uint height)
{
if (width == 0 || height == 0)
{
return;
}
lock (_gate)
{
if (_closed || _thread is not null)
{
return;
}
}
var hasSplash = PngSplashLoader.TryLoad(
out var splashPixels,
out var splashWidth,
out var splashHeight);
lock (_gate)
{
if (_closed || _thread is not null)
{
return;
}
_windowWidth = width;
_windowHeight = height;
_latestPresentation ??= hasSplash
? new Presentation(
splashPixels,
splashWidth,
splashHeight,
1,
GuestDrawKind.None,
IsSplash: true)
: new Presentation(
null,
width,
height,
0,
GuestDrawKind.None,
IsSplash: false);
_thread = new Thread(Run)
{
IsBackground = true,
Name = "SharpEmu Vulkan VideoOut",
};
_thread.Start();
}
}
public static void Submit(byte[] bgraFrame, uint width, uint height)
{
if (bgraFrame.Length != checked((int)(width * height * 4)))
@@ -40,12 +94,20 @@ internal static unsafe class VulkanVideoPresenter
}
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
_latestPresentation = new Presentation(bgraFrame, width, height, sequence, GuestDrawKind.None);
_latestPresentation = new Presentation(
bgraFrame,
width,
height,
sequence,
GuestDrawKind.None,
IsSplash: false);
if (_thread is not null)
{
return;
}
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run)
{
IsBackground = true,
@@ -74,12 +136,20 @@ internal static unsafe class VulkanVideoPresenter
}
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
_latestPresentation = new Presentation(null, width, height, sequence, drawKind);
_latestPresentation = new Presentation(
null,
width,
height,
sequence,
drawKind,
IsSplash: false);
if (_thread is not null)
{
return;
}
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run)
{
IsBackground = true,
@@ -95,8 +165,8 @@ internal static unsafe class VulkanVideoPresenter
uint height;
lock (_gate)
{
width = _latestPresentation?.Width ?? 1280;
height = _latestPresentation?.Height ?? 720;
width = _windowWidth == 0 ? _latestPresentation?.Width ?? 1280 : _windowWidth;
height = _windowHeight == 0 ? _latestPresentation?.Height ?? 720 : _windowHeight;
}
try
@@ -138,7 +208,8 @@ internal static unsafe class VulkanVideoPresenter
uint Width,
uint Height,
long Sequence,
GuestDrawKind DrawKind);
GuestDrawKind DrawKind,
bool IsSplash);
private sealed class Presenter : IDisposable
{
@@ -179,6 +250,7 @@ internal static unsafe class VulkanVideoPresenter
private bool _vulkanReady;
private bool _firstFramePresented;
private bool _firstGuestDrawPresented;
private bool _splashPresented;
public Presenter(uint width, uint height)
{
@@ -813,7 +885,14 @@ internal static unsafe class VulkanVideoPresenter
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
_imageInitialized[imageIndex] = true;
_presentedSequence = presentation.Sequence;
if (!_firstFramePresented)
if (presentation.IsSplash && !_splashPresented)
{
_splashPresented = true;
Console.Error.WriteLine(
$"[LOADER][INFO] Vulkan VideoOut presented splash: " +
$"{presentation.Width}x{presentation.Height}");
}
else if (!presentation.IsSplash && !_firstFramePresented)
{
_firstFramePresented = true;
Console.Error.WriteLine(