Compare commits

..

43 Commits

Author SHA1 Message Date
ParantezTech 5256ab8dec [core] Update native execution and kernel exports, phtread improvement 2026-07-03 12:39:53 +03:00
ParantezTech 0d89b2488b [dotnet] remove shaderc package because it is no longer needed 2026-07-02 17:15:51 +03:00
ParantezTech c3019b78d6 [ampr] more improvements to the Ampr library, generally for performance 2026-07-01 13:50:52 +03:00
ParantezTech dd5e524879 [pthread] pthread improvements and fixes 2026-07-01 13:50:24 +03:00
ParantezTech df9d0e6aaf [NpEntitlementAccess] added sceNpEntitlementAccessGetAddcontEntitlementInfoList export 2026-07-01 13:49:49 +03:00
ParantezTech 253d0fae3f [saveData] minimal save data dialog exports 2026-07-01 13:49:20 +03:00
ParantezTech 78d719ef9e [core] more leaf for speedup 2026-07-01 13:48:45 +03:00
ParantezTech be3806cdf6 [packages] added Silk.NET.Shaderc for debugging GLSL 2026-07-01 13:45:13 +03:00
ParantezTech dc47deee93 [agc] new imports for shader translation 2026-06-29 18:29:05 +03:00
ParantezTech 1c838cf8d6 [core] more leaf imports 2026-06-29 18:13:21 +03:00
ParantezTech 2f269566a5 [readme] update Demon's Souls text 2026-06-29 18:10:46 +03:00
ParantezTech 9d7afef6f6 [readme] update Demon's Souls new boot info 2026-06-29 14:40:27 +03:00
ParantezTech daeff21516 [readme] update screenshots 2026-06-29 14:34:36 +03:00
ParantezTech 82619deb34 [ci] ignore image files from actions 2026-06-29 14:32:46 +03:00
ParantezTech 697ad7be80 [libs] Add NP entitlement validation 2026-06-29 14:31:37 +03:00
ParantezTech 73c987e49f [libs] Improve video output handling 2026-06-29 14:31:26 +03:00
ParantezTech cd79275117 [libs] Enhance PlayGo streaming service 2026-06-29 14:31:18 +03:00
ParantezTech b14ecae504 [core] Refine native CPU execution imports 2026-06-29 14:31:10 +03:00
ParantezTech 873f473b65 [video] hide splash 2026-06-29 13:32:17 +03:00
ParantezTech 9a1a3789ef [video] cap presenter ticks 2026-06-29 13:32:03 +03:00
ParantezTech 0f3d4032cb [core] add leaf imports 2026-06-29 13:30:50 +03:00
ParantezTech 0c4a757695 [core] log leaf import args 2026-06-29 13:30:32 +03:00
ParantezTech 5fcc8eaa37 [saveData] add dir search 2026-06-29 13:30:08 +03:00
ParantezTech 21105e0346 [ampr] batch buffer IO 2026-06-29 13:28:37 +03:00
ParantezTech b424df5a64 [kernel] speed up printf 2026-06-29 13:28:30 +03:00
ParantezTech 7f85971e39 [kernel] carry pthread scheduling 2026-06-29 13:27:59 +03:00
ParantezTech e28259a99d [core] speed up guest memory 2026-06-29 13:26:04 +03:00
ParantezTech 3d2a4b74ac [readme] add screenshot running Demon's Souls 2026-06-29 00:17:13 +03:00
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
45 changed files with 8305 additions and 797 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 933 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

+6
View File
@@ -9,11 +9,17 @@ on:
- "**" - "**"
paths-ignore: paths-ignore:
- "**/*.md" - "**/*.md"
- "**/*.png"
- "**/*.jpeg"
- "**/*.jpg"
pull_request: pull_request:
branches: branches:
- main - main
paths-ignore: paths-ignore:
- "**/*.md" - "**/*.md"
- "**/*.png"
- "**/*.jpeg"
- "**/*.jpg"
workflow_dispatch: workflow_dispatch:
permissions: permissions:
BIN
View File
Binary file not shown.
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System; using System;
using System.Collections.Concurrent;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
@@ -12,10 +13,23 @@ namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend 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; _recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
if (_recentImportTraceCount < _recentImportTrace.Length) if (_recentImportTraceCount < _recentImportTrace.Length)
{ {
@@ -34,10 +48,12 @@ public sealed partial class DirectExecutionBackend
for (int i = 0; i < _recentImportTraceCount; i++) for (int i = 0; i < _recentImportTraceCount; i++)
{ {
int num2 = (num + i) % _recentImportTrace.Length; int num2 = (num + i) % _recentImportTrace.Length;
string text = _recentImportTrace[num2]; var entry = _recentImportTrace[num2];
if (!string.IsNullOrEmpty(text)) 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) 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) if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{ {
return false; 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) private static ulong AlignUp(ulong value, ulong alignment)
@@ -15,6 +15,9 @@ namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend public sealed partial class DirectExecutionBackend
{ {
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
private static int _lazyCommitTraceCount;
private unsafe void SetupExceptionHandler() private unsafe void SetupExceptionHandler()
{ {
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal)) if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
@@ -911,7 +914,21 @@ public sealed partial class DirectExecutionBackend
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL; ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect); 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; bool committed = false;
ulong committedBase = 0; ulong committedBase = 0;
@@ -919,14 +936,25 @@ public sealed partial class DirectExecutionBackend
if (mbi.State == 65536) if (mbi.State == 65536)
{ {
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL; if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect)) TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
{ {
committed = true; committed = true;
committedBase = largeBase; committedBase = windowBase;
committedSize = 2097152uL; committedSize = windowSize;
} }
else else
{
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeBase;
committedSize = 2097152uL;
}
}
if (!committed)
{ {
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL; ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect)) if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
@@ -949,7 +977,10 @@ public sealed partial class DirectExecutionBackend
} }
TryCommitRange(pageBase + 4096, 4096uL, commitProtect); 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; return true;
} }
@@ -958,14 +989,25 @@ public sealed partial class DirectExecutionBackend
return false; return false;
} }
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL; if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect)) TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
{ {
committed = true; committed = true;
committedBase = largeCommitBase; committedBase = commitWindowBase;
committedSize = 2097152uL; committedSize = commitWindowSize;
} }
else else
{
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
{
committed = true;
committedBase = largeCommitBase;
committedSize = 2097152uL;
}
}
if (!committed)
{ {
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL; ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
if (TryCommitRange(region64kBase, 65536uL, commitProtect)) if (TryCommitRange(region64kBase, 65536uL, commitProtect))
@@ -994,9 +1036,46 @@ public sealed partial class DirectExecutionBackend
} }
TryCommitRange(pageBase + 4096, 4096uL, commitProtect); 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; 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) static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
{ {
if (length == 0) if (length == 0)
@@ -1023,6 +1102,49 @@ public sealed partial class DirectExecutionBackend
} }
return TryCommitRange(commitAddress, commitSize, protection); 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) private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
@@ -14,6 +14,9 @@ namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend 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) private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
{ {
try try
@@ -73,8 +76,11 @@ public sealed partial class DirectExecutionBackend
private unsafe ulong DispatchImport(int importIndex, nint argPackPtr) private unsafe ulong DispatchImport(int importIndex, nint argPackPtr)
{ {
long num = Interlocked.Increment(ref _importDispatchCount); long num = NextImportDispatchIndex();
MarkExecutionProgress(); if ((num & 0x3F) == 0)
{
MarkExecutionProgress();
}
var cpuContext = ActiveCpuContext; var cpuContext = ActiveCpuContext;
if (cpuContext == null) 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})"); Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
_lastReportedRawSentinelRecoveries = num2; _lastReportedRawSentinelRecoveries = num2;
} }
if (IsLeafImport(importStubEntry.Nid) &&
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
{
return leafResult;
}
cpuContext.Rip = importStubEntry.Address; cpuContext.Rip = importStubEntry.Address;
cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr; cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8); cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
@@ -106,6 +118,10 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72); cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80); cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88); cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
cpuContext.SetXmmRegister(
0,
*(ulong*)(argPackPtr - 16),
*(ulong*)(argPackPtr - 8));
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL; cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
ulong value = cpuContext[CpuRegister.Rdi]; ulong value = cpuContext[CpuRegister.Rdi];
ulong value2 = cpuContext[CpuRegister.Rsi]; ulong value2 = cpuContext[CpuRegister.Rsi];
@@ -120,6 +136,7 @@ public sealed partial class DirectExecutionBackend
ulong value7 = cpuContext[CpuRegister.R14]; ulong value7 = cpuContext[CpuRegister.R14];
ulong value8 = cpuContext[CpuRegister.R15]; ulong value8 = cpuContext[CpuRegister.R15];
ulong num7 = *(ulong*)(argPackPtr + 96); ulong num7 = *(ulong*)(argPackPtr + 96);
var isGuestWorker = GuestThreadExecution.IsGuestThread;
if (!IsLikelyReturnAddress(num7)) if (!IsLikelyReturnAddress(num7))
{ {
for (int i = 1; i <= 4; i++) for (int i = 1; i <= 4; i++)
@@ -134,19 +151,29 @@ public sealed partial class DirectExecutionBackend
} }
} }
} }
TrackDistinctImportNid(importStubEntry.Nid); if (_activeGuestThreadState is { } activeGuestThreadState)
var probeImportReturn = Environment.GetEnvironmentVariable("SHARPEMU_PROBE_IMPORT_RET"); {
if (!string.IsNullOrWhiteSpace(probeImportReturn) && Interlocked.Increment(ref activeGuestThreadState.ImportCount);
(string.Equals(probeImportReturn, "*", StringComparison.Ordinal) || Volatile.Write(ref activeGuestThreadState.LastImportNid, importStubEntry.Nid);
string.Equals(probeImportReturn, importStubEntry.Nid, StringComparison.Ordinal))) 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); ProbeReturnRip(num7, num);
} }
TrackStrlenPrelude(importStubEntry.Nid, num, num7); 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}"); TraceGuestContext(
bool logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal); $"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)) }
if (_logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
{ {
string symbolText = "<unreadable>"; string symbolText = "<unreadable>";
if (TryReadAsciiZ(value2, 256, out var sym)) if (TryReadAsciiZ(value2, 256, out var sym))
@@ -156,7 +183,10 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine( 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}"); $"[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; cpuContext[CpuRegister.Rax] = 1uL;
return 1uL; return 1uL;
@@ -165,27 +195,33 @@ public sealed partial class DirectExecutionBackend
bool flag = num7 >= 2156221920u && num7 <= 2156225024u; bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u; bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
bool flag3 = num >= 1020 && num <= 1040; bool flag3 = num >= 1020 && num <= 1040;
bool logAllImports = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_ALL_IMPORTS"), "1", StringComparison.Ordinal); bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
string importFilter = Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FILTER");
bool flag4 = !string.IsNullOrWhiteSpace(importFilter);
bool flag5 = false; bool flag5 = false;
ExportedFunction matchedExport = null; ExportedFunction? matchedExport = importStubEntry.Export;
if (_moduleManager.TryGetExport(importStubEntry.Nid, out ExportedFunction 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) if (flag4)
{ {
flag5 = export.LibraryName.Contains(importFilter, StringComparison.OrdinalIgnoreCase) flag5 = matchedExport.LibraryName.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase)
|| export.Name.Contains(importFilter, StringComparison.OrdinalIgnoreCase) || matchedExport.Name.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase)
|| importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase); || importStubEntry.Nid.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase);
} }
} }
else if (flag4) else if (flag4)
{ {
flag5 = importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase); flag5 = importStubEntry.Nid.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase);
} }
bool flag6 = logAllImports || flag5; 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)) if (!flag0 && (flag6 || periodicTrace))
{ {
if (matchedExport != null) if (matchedExport != null)
{ {
@@ -218,9 +254,15 @@ public sealed partial class DirectExecutionBackend
Console.Error.Flush(); 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) if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
{ {
@@ -247,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}"); 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); TraceImportFrameChain(cpuContext, num);
} }
if (flag6 && string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_RECENT"), "1", StringComparison.Ordinal)) if (flag6 && _logImportRecent)
{ {
DumpRecentImportTrace(); DumpRecentImportTrace();
} }
@@ -262,7 +304,7 @@ public sealed partial class DirectExecutionBackend
} }
if (importStubEntry.Nid == "Ou3iL1abvng") 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 savedGuardAddress = value4 >= 0x10 ? value4 - 0x10 : 0;
var guardKnown = TryReadUInt64Compat(value3, out var guardValue); var guardKnown = TryReadUInt64Compat(value3, out var guardValue);
@@ -300,9 +342,22 @@ public sealed partial class DirectExecutionBackend
{ {
orbisGen2Result = DispatchKernelDynlibDlsym(); 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 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 finally
@@ -318,7 +373,9 @@ public sealed partial class DirectExecutionBackend
if (!dispatchResolved) if (!dispatchResolved)
{ {
LastError = "Missing HLE export for NID: " + importStubEntry.Nid; 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") if (importStubEntry.Nid == "L-Q3LEjIbgA")
{ {
string value18 = string.Join(" ", importStubEntry.Nid.Select(delegate (char c) string value18 = string.Join(" ", importStubEntry.Nid.Select(delegate (char c)
@@ -337,9 +394,12 @@ public sealed partial class DirectExecutionBackend
} }
else if (orbisGen2Result != OrbisGen2Result.ORBIS_GEN2_OK) else if (orbisGen2Result != OrbisGen2Result.ORBIS_GEN2_OK)
{ {
Console.Error.WriteLine( if (ShouldLogImportResult(importStubEntry.Nid, orbisGen2Result))
$"[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}"); 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.Rbx] = value3;
cpuContext[CpuRegister.Rbp] = value4; cpuContext[CpuRegister.Rbp] = value4;
@@ -349,11 +409,36 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.R15] = value8; cpuContext[CpuRegister.R15] = value8;
cpuContext[CpuRegister.Rdi] = value; cpuContext[CpuRegister.Rdi] = value;
cpuContext[CpuRegister.Rsi] = value2; 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 else
{ {
@@ -361,9 +446,27 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rax] = 18446744071562199298uL; 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)) TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
{ {
if (hasBlockContinuation)
{
RegisterBlockedGuestThreadContinuation(
GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation,
blockWakeKey,
blockResumeHandler,
blockWakeHandler,
blockDeadlineTimestamp);
}
cpuContext[CpuRegister.Rax] = 0uL; cpuContext[CpuRegister.Rax] = 0uL;
} }
if (flag || flag2 || flag3) if (flag || flag2 || flag3)
@@ -379,11 +482,304 @@ public sealed partial class DirectExecutionBackend
catch (Exception ex) catch (Exception ex)
{ {
LastError = $"HLE dispatch error for {importStubEntry.Nid}: {ex.GetType().Name}: {ex.Message}"; LastError = $"HLE dispatch error for {importStubEntry.Nid}: {ex.GetType().Name}: {ex.Message}";
Console.Error.WriteLine($"[LOADER][ERROR] {LastError}");
Console.Error.WriteLine($"[LOADER][ERROR] {ex.StackTrace}");
cpuContext[CpuRegister.Rax] = 18446744071562199298uL; cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
return 18446744071562199298uL; return 18446744071562199298uL;
} }
} }
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}) " +
$"rdi=0x{arg0:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} " +
$"rdx=0x{cpuContext[CpuRegister.Rdx]:X16} rcx=0x{cpuContext[CpuRegister.Rcx]:X16} " +
$"ret=0x{returnRip:X16}");
}
int returnValue;
if (IsNoBlockLeafImport(importStubEntry.Nid))
{
cpuContext.ClearRaxWriteFlag();
returnValue = export.Function(cpuContext);
if (!cpuContext.WasRaxWritten)
{
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
}
}
else
{
var previousImportCallFrame = GuestThreadExecution.EnterImportCallFrame(
returnRip,
(ulong)argPackPtr + 104uL,
ActiveGuestReturnSlotAddress);
try
{
cpuContext.ClearRaxWriteFlag();
returnValue = export.Function(cpuContext);
if (!cpuContext.WasRaxWritten)
{
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
}
}
finally
{
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
}
}
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
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 static bool IsNoBlockLeafImport(string nid) =>
nid is
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
"a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
"Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
"GuchCTefuZw" or // sceAmprCommandBufferDestructor
"N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
"baQO9ez2gL4" or // sceAmprCommandBufferReset
"ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
"mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"qvMUCyyaCSI"; // sceKernelAprSubmitCommandBufferAndGetId
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
{
var expectedFileProbeMiss =
result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND &&
IsExpectedFileProbeNotFoundNid(nid);
var expectedTimedWaitTimeout =
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
unchecked((int)result) == 60;
var expectedMutexTrylockBusy =
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
if (!expectedFileProbeMiss && !expectedTimedWaitTimeout && !expectedMutexTrylockBusy)
{
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
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
"qj7QZpgr9Uw" or // Gen5 graphics type-2 packet
"LtTouSCZjHM" or // sceAgcCbNop
"k3GhuSNmBLU" or // sceAgcCbDispatch
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
"JrtiDtKeS38" or // sceAgcAcbResetQueue
"cFazmnXpJOE" or // sceAgcAcbEventWrite
"KT-hTp-Ch14" or // sceAgcAcbAcquireMem
"htn36gPnBk4" or // sceAgcAcbWaitRegMem
"eZ4+17OQz4Q" or // sceAgcAcbWriteData
"j3EtxFkSIhQ" or // sceAgcAcbDispatchIndirect
"gSRnr79F8tQ" or // sceAgcDriverSubmitAcb
"i1jyy49AjXU" or // sceAgcDcbWriteData
"VmW0Tdpy420" or // sceAgcDcbWaitRegMem
"WmAc2MEj6Io" or // sceAgcDcbDmaData
"rUuVjyR+Rd4" or // sceAgcDcbGetLodStatsGetSize
"vuSXe69VILM" or // sceAgcDcbGetLodStats
"RmaJwLtc8rY" or // sceAgcDcbSetBaseIndirectArgs
"CtB+A9-VxO0" or // sceAgcDcbDispatchIndirect
"+kSrjIVxKFE" or // sceAgcDcbPushMarker
"H7uZqCoNuWk" or // sceAgcDcbPopMarker
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
"a8uLzYY--tM" or
"Qs1xtplKo0U" or
"GuchCTefuZw" or
"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
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
"TywrFKCoLGY" or // sceSaveDataInitialize3
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
"ZP4e7rlzOUk" or // sceSaveDataMount3
"ERKzksauAJA" or // sceSaveDataDialogGetStatus
"KK3Bdg1RWK0" or // sceSaveDataDialogUpdateStatus
"en7gNVnh878" or // sceSaveDataDialogIsReadyToDisplay
"jO8DM8oyego" or // sceNpEntitlementAccessInitialize
"TFyU+KFBv54" or // sceNpEntitlementAccessGetAddcontEntitlementInfoList
"27bAgiJmOh0" or // pthread_cond_timedwait
"iQw3iQPhvUQ" or // sceNetCtlCheckCallback
"Q2V+iqvjgC0" or // vsnprintf
"j4ViWNHEgww" or // strlen
"5jNubw4vlAA" or // strnlen
"LHMrG7e8G78" or // wcslen
"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) private void TraceImportFrameChain(CpuContext context, long dispatchIndex)
{ {
var frame = context[CpuRegister.Rbp]; var frame = context[CpuRegister.Rbp];
@@ -431,7 +827,7 @@ public sealed partial class DirectExecutionBackend
return true; 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; ulong hostExit = ActiveEntryReturnSentinelRip;
if (hostExit < 65536 || !TryPatchActiveGuestReturnSlot(hostExit)) if (hostExit < 65536 || !TryPatchActiveGuestReturnSlot(hostExit))
@@ -447,7 +843,7 @@ public sealed partial class DirectExecutionBackend
return false; return false;
} }
Console.Error.WriteLine( 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; return true;
} }
@@ -469,8 +865,11 @@ public sealed partial class DirectExecutionBackend
ActiveGuestThreadYieldRequested = true; ActiveGuestThreadYieldRequested = true;
ActiveGuestThreadYieldReason = string.IsNullOrWhiteSpace(reason) ? nid : reason; ActiveGuestThreadYieldReason = string.IsNullOrWhiteSpace(reason) ? nid : reason;
Console.Error.WriteLine( if (_logGuestThreads)
$"[LOADER][INFO] Guest thread yield at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={ActiveGuestThreadYieldReason}"); {
Console.Error.WriteLine(
$"[LOADER][INFO] Guest thread yield at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={ActiveGuestThreadYieldReason}");
}
return true; return true;
} }
@@ -488,7 +887,7 @@ public sealed partial class DirectExecutionBackend
{ {
return false; return false;
} }
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"), "1", StringComparison.Ordinal)) if (_disableImportLoopGuard || _importLoopGuardSeconds <= 0)
{ {
return false; return false;
} }
@@ -503,6 +902,10 @@ public sealed partial class DirectExecutionBackend
_importNidHashCache[nid] = value; _importNidHashCache[nid] = value;
} }
RecordImportLoopSignature(value, returnRip, BuildImportLoopSignature(value, returnRip, arg0, arg1)); RecordImportLoopSignature(value, returnRip, BuildImportLoopSignature(value, returnRip, arg0, arg1));
if ((dispatchIndex & 0x3F) != 0)
{
return false;
}
if (!HasRepeatingImportLoopPattern()) if (!HasRepeatingImportLoopPattern())
{ {
if (_importLoopPatternHits > 0) if (_importLoopPatternHits > 0)
@@ -520,14 +923,13 @@ public sealed partial class DirectExecutionBackend
_importLoopPatternStartTimestamp = Stopwatch.GetTimestamp(); _importLoopPatternStartTimestamp = Stopwatch.GetTimestamp();
} }
_importLoopPatternHits++; _importLoopPatternHits++;
var guardSeconds = GetImportLoopGuardSeconds(); if (_importLoopPatternHits < 6)
if (guardSeconds <= 0 || _importLoopPatternHits < 6)
{ {
return false; return false;
} }
var elapsedTicks = Stopwatch.GetTimestamp() - _importLoopPatternStartTimestamp; var elapsedTicks = Stopwatch.GetTimestamp() - _importLoopPatternStartTimestamp;
return elapsedTicks >= (long)(guardSeconds * Stopwatch.Frequency); return elapsedTicks >= (long)(_importLoopGuardSeconds * Stopwatch.Frequency);
} }
private static bool IsImportLoopGuardBoundary(string nid) => private static bool IsImportLoopGuardBoundary(string nid) =>
@@ -810,8 +1212,7 @@ public sealed partial class DirectExecutionBackend
{ {
return result; return result;
} }
bool logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal); if (_logBootstrap)
if (logBootstrap)
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}"); $"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
File diff suppressed because it is too large Load Diff
+269 -47
View File
@@ -9,7 +9,7 @@ namespace SharpEmu.Core.Memory;
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
{ {
private readonly object _gate = new(); private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
private readonly object _guestAllocationGate = new(); private readonly object _guestAllocationGate = new();
private readonly List<MemoryRegion> _regions = new(); private readonly List<MemoryRegion> _regions = new();
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new(); private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
@@ -78,7 +78,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return false; return false;
} }
lock (_gate) _gate.EnterWriteLock();
try
{ {
_regions.Add(new MemoryRegion _regions.Add(new MemoryRegion
{ {
@@ -89,6 +90,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
Protection = protection Protection = protection
}); });
} }
finally
{
_gate.ExitWriteLock();
}
var allocationKind = executable ? "executable memory" : "data memory"; var allocationKind = executable ? "executable memory" : "data memory";
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)"); Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
@@ -202,7 +207,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
} }
lock (_gate) _gate.EnterWriteLock();
try
{ {
_regions.Add(new MemoryRegion _regions.Add(new MemoryRegion
{ {
@@ -213,6 +219,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
Protection = protection Protection = protection
}); });
} }
finally
{
_gate.ExitWriteLock();
}
var allocationKind = reservedOnly var allocationKind = reservedOnly
? "reserved data memory (lazy commit)" ? "reserved data memory (lazy commit)"
@@ -315,7 +325,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
{ {
lock (_guestAllocationGate) lock (_guestAllocationGate)
{ {
lock (_gate) _gate.EnterWriteLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -324,6 +335,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_regions.Clear(); _regions.Clear();
_pageProtections.Clear(); _pageProtections.Clear();
} }
finally
{
_gate.ExitWriteLock();
}
_guestAllocationArenaBase = 0; _guestAllocationArenaBase = 0;
_guestAllocationOffset = 0; _guestAllocationOffset = 0;
@@ -343,7 +358,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var mapEnd = AlignUp(segmentEnd, PageSize); var mapEnd = AlignUp(segmentEnd, PageSize);
var mapSize = checked(mapEnd - mapStart); var mapSize = checked(mapEnd - mapStart);
lock (_gate) _gate.EnterWriteLock();
try
{ {
var existingRegion = FindRegion(mapStart, mapSize); var existingRegion = FindRegion(mapStart, mapSize);
if (existingRegion == null) if (existingRegion == null)
@@ -376,6 +392,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})"); Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
} }
finally
{
_gate.ExitWriteLock();
}
} }
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags) private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
@@ -425,7 +445,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions() public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
{ {
lock (_gate) _gate.EnterReadLock();
try
{ {
var snapshot = new VirtualMemoryRegion[_regions.Count]; var snapshot = new VirtualMemoryRegion[_regions.Count];
for (var i = 0; i < _regions.Count; i++) for (var i = 0; i < _regions.Count; i++)
@@ -440,11 +461,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
return snapshot; return snapshot;
} }
finally
{
_gate.ExitReadLock();
}
} }
public bool TryRead(ulong virtualAddress, Span<byte> destination) public bool TryRead(ulong virtualAddress, Span<byte> destination)
{ {
lock (_gate) var requiresExclusiveAccess = false;
_gate.EnterReadLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -456,38 +483,55 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true; return true;
} }
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region)) if (region.IsReservedOnly)
{ {
return false; if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
}
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
{
return false;
}
try
{
fixed (byte* destPtr = destination)
{ {
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length); return false;
} }
} }
finally
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
{ {
RestorePageProtections(touchedPages); requiresExclusiveAccess = true;
break;
}
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
} }
return true; return true;
} }
} }
}
finally
{
_gate.ExitReadLock();
}
if (!requiresExclusiveAccess)
{
return false; return false;
} }
_gate.EnterWriteLock();
try
{
return TryReadExclusive(virtualAddress, destination);
}
finally
{
_gate.ExitWriteLock();
}
} }
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source) public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{ {
lock (_gate) var requiresExclusiveAccess = false;
_gate.EnterReadLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -499,37 +543,148 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return true; return true;
} }
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region)) if (region.IsReservedOnly)
{ {
return false; if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
}
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
{
return false;
}
try
{
fixed (byte* srcPtr = source)
{ {
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length); return false;
} }
} }
finally
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
{ {
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _); requiresExclusiveAccess = true;
if (IsExecutableProtection(oldProtect)) break;
{ }
FlushInstructionCache(null, destPtr, (nuint)source.Length);
} fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
} }
return true; return true;
} }
} }
}
finally
{
_gate.ExitReadLock();
}
if (!requiresExclusiveAccess)
{
return false; return false;
} }
_gate.EnterWriteLock();
try
{
return TryWriteExclusive(virtualAddress, source);
}
finally
{
_gate.ExitWriteLock();
}
}
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
{
foreach (var region in _regions)
{
if (!TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
{
continue;
}
var srcPtr = (void*)(region.VirtualAddress + offset);
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
return false;
}
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
{
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
return true;
}
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
{
return false;
}
try
{
fixed (byte* destPtr = destination)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
}
}
finally
{
RestorePageProtections(touchedPages);
}
return true;
}
return false;
}
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
{
foreach (var region in _regions)
{
if (!TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
{
continue;
}
var destPtr = (void*)(region.VirtualAddress + offset);
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
return false;
}
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
{
fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
return true;
}
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
{
return false;
}
try
{
fixed (byte* srcPtr = source)
{
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
}
}
finally
{
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect))
{
FlushInstructionCache(null, destPtr, (nuint)source.Length);
}
}
return true;
}
return false;
} }
public bool TryWriteUInt64(ulong virtualAddress, ulong value) public bool TryWriteUInt64(ulong virtualAddress, ulong value)
@@ -541,7 +696,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public void* GetPointer(ulong virtualAddress) public void* GetPointer(ulong virtualAddress)
{ {
lock (_gate) _gate.EnterReadLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -553,11 +709,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
return null; return null;
} }
finally
{
_gate.ExitReadLock();
}
} }
public bool IsAccessible(ulong virtualAddress, ulong size) public bool IsAccessible(ulong virtualAddress, ulong size)
{ {
lock (_gate) _gate.EnterReadLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -568,6 +729,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
return false; return false;
} }
finally
{
_gate.ExitReadLock();
}
} }
private MemoryRegion? FindRegion(ulong address, ulong size) private MemoryRegion? FindRegion(ulong address, ulong size)
@@ -591,7 +756,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
var end = address + size; var end = address + size;
lock (_gate) _gate.EnterReadLock();
try
{ {
foreach (var region in _regions) foreach (var region in _regions)
{ {
@@ -602,6 +768,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
} }
} }
} }
finally
{
_gate.ExitReadLock();
}
return overlapEnd != 0; return overlapEnd != 0;
} }
@@ -633,6 +803,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; 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) private static uint GetCommitProtection(MemoryRegion region)
{ {
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE; return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
@@ -649,15 +857,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var endPage = AlignUp(address + size, PageSize); var endPage = AlignUp(address + size, PageSize);
var commitProtection = GetCommitProtection(region); var commitProtection = GetCommitProtection(region);
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize) var pageAddress = startPage;
while (pageAddress < endPage)
{ {
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0) if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
{ {
return false; return false;
} }
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
? ulong.MaxValue
: info.BaseAddress + info.RegionSize;
var rangeEnd = Math.Min(endPage, queriedEnd);
if (rangeEnd <= pageAddress)
{
return false;
}
if (info.State == MEM_COMMIT) if (info.State == MEM_COMMIT)
{ {
pageAddress = rangeEnd;
continue; continue;
} }
@@ -666,10 +885,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
return false; return false;
} }
if (VirtualAlloc((void*)pageAddress, (nuint)PageSize, MEM_COMMIT, commitProtection) == null) var commitSize = rangeEnd - pageAddress;
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
{ {
return false; return false;
} }
pageAddress = rangeEnd;
} }
return true; return true;
@@ -10,6 +10,7 @@ using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut; using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel; using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent; using SharpEmu.Libs.AppContent;
using SharpEmu.Libs.SaveData;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq; using System.Linq;
@@ -136,6 +137,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
KernelModuleRegistry.Reset(); KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath); var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version); VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false); RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress); KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}"); Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
+237 -9
View File
@@ -1,6 +1,8 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
namespace SharpEmu.HLE; namespace SharpEmu.HLE;
public readonly record struct GuestThreadStartRequest( public readonly record struct GuestThreadStartRequest(
@@ -8,14 +10,37 @@ public readonly record struct GuestThreadStartRequest(
ulong EntryPoint, ulong EntryPoint,
ulong Argument, ulong Argument,
ulong AttributeAddress, ulong AttributeAddress,
string Name); string Name,
int Priority,
ulong AffinityMask);
public readonly record struct GuestThreadSnapshot(
ulong ThreadHandle,
string Name,
string State,
long ImportCount,
string? LastImportNid,
ulong LastReturnRip,
string? BlockReason);
public interface IGuestThreadScheduler public interface IGuestThreadScheduler
{ {
bool SupportsGuestContextTransfer { get; }
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error); bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
bool TryJoinThread(
CpuContext callerContext,
ulong threadHandle,
out ulong returnValue,
out string? error);
void Pump(CpuContext callerContext, string reason); void Pump(CpuContext callerContext, string reason);
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
bool TryCallGuestFunction( bool TryCallGuestFunction(
CpuContext callerContext, CpuContext callerContext,
ulong entryPoint, ulong entryPoint,
@@ -71,15 +96,39 @@ public static class GuestThreadExecution
[ThreadStatic] [ThreadStatic]
private static string? _pendingBlockReason; 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] [ThreadStatic]
private static bool _pendingEntryExit; private static bool _pendingEntryExit;
[ThreadStatic] [ThreadStatic]
private static int _pendingEntryExitStatus; private static ulong _pendingEntryExitValue;
[ThreadStatic] [ThreadStatic]
private static string? _pendingEntryExitReason; private static string? _pendingEntryExitReason;
[ThreadStatic]
private static bool _pendingContextTransfer;
[ThreadStatic]
private static GuestCpuContinuation _pendingContextTransferTarget;
[ThreadStatic] [ThreadStatic]
private static bool _hasCurrentImportCallFrame; private static bool _hasCurrentImportCallFrame;
@@ -105,9 +154,17 @@ public static class GuestThreadExecution
var previous = _currentGuestThreadHandle; var previous = _currentGuestThreadHandle;
_currentGuestThreadHandle = threadHandle; _currentGuestThreadHandle = threadHandle;
_pendingBlockReason = null; _pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false; _pendingEntryExit = false;
_pendingEntryExitStatus = 0; _pendingEntryExitValue = 0;
_pendingEntryExitReason = null; _pendingEntryExitReason = null;
_pendingContextTransfer = false;
_pendingContextTransferTarget = default;
_hasCurrentImportCallFrame = false; _hasCurrentImportCallFrame = false;
_currentImportReturnRip = 0; _currentImportReturnRip = 0;
_currentImportResumeRsp = 0; _currentImportResumeRsp = 0;
@@ -119,9 +176,17 @@ public static class GuestThreadExecution
{ {
_currentGuestThreadHandle = previousThreadHandle; _currentGuestThreadHandle = previousThreadHandle;
_pendingBlockReason = null; _pendingBlockReason = null;
_pendingBlockContinuationValid = false;
_pendingBlockContinuation = default;
_pendingBlockWakeKey = null;
_pendingBlockResumeHandler = null;
_pendingBlockWakeHandler = null;
_pendingBlockDeadlineTimestamp = 0;
_pendingEntryExit = false; _pendingEntryExit = false;
_pendingEntryExitStatus = 0; _pendingEntryExitValue = 0;
_pendingEntryExitReason = null; _pendingEntryExitReason = null;
_pendingContextTransfer = false;
_pendingContextTransferTarget = default;
_hasCurrentImportCallFrame = false; _hasCurrentImportCallFrame = false;
_currentImportReturnRip = 0; _currentImportReturnRip = 0;
_currentImportResumeRsp = 0; _currentImportResumeRsp = 0;
@@ -140,7 +205,15 @@ public static class GuestThreadExecution
_currentFiberAddress = previousFiberAddress; _currentFiberAddress = previousFiberAddress;
} }
public static bool RequestCurrentThreadBlock(string reason) 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) if (!IsGuestThread)
{ {
@@ -148,31 +221,167 @@ public static class GuestThreadExecution
} }
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason; _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; return true;
} }
public static bool TryConsumeCurrentThreadBlock(out string reason) 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; reason = _pendingBlockReason ?? string.Empty;
if (string.IsNullOrEmpty(reason)) if (string.IsNullOrEmpty(reason))
{ {
continuation = default;
hasContinuation = false;
wakeKey = string.Empty;
resumeHandler = null;
wakeHandler = null;
blockDeadlineTimestamp = 0;
return false; return false;
} }
continuation = _pendingBlockContinuation;
hasContinuation = _pendingBlockContinuationValid;
wakeKey = _pendingBlockWakeKey ?? reason;
resumeHandler = _pendingBlockResumeHandler;
wakeHandler = _pendingBlockWakeHandler;
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
_pendingBlockReason = null; _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; return true;
} }
public static void RequestCurrentEntryExit(string reason, int status) public static void RequestCurrentEntryExit(string reason, int status)
{
RequestCurrentEntryExit(reason, unchecked((ulong)(long)status));
}
public static void RequestCurrentEntryExit(string reason, ulong value)
{ {
_pendingEntryExit = true; _pendingEntryExit = true;
_pendingEntryExitStatus = status; _pendingEntryExitValue = value;
_pendingEntryExitReason = string.IsNullOrWhiteSpace(reason) ? "guest_entry_exit" : reason; _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; reason = _pendingEntryExitReason ?? string.Empty;
if (!_pendingEntryExit) if (!_pendingEntryExit)
{ {
@@ -180,11 +389,30 @@ public static class GuestThreadExecution
} }
_pendingEntryExit = false; _pendingEntryExit = false;
_pendingEntryExitStatus = 0; _pendingEntryExitValue = 0;
_pendingEntryExitReason = null; _pendingEntryExitReason = null;
return true; return true;
} }
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( public static GuestImportCallFrame EnterImportCallFrame(
ulong returnRip, ulong returnRip,
ulong resumeRsp, ulong resumeRsp,
File diff suppressed because it is too large Load Diff
+91 -46
View File
@@ -3,6 +3,7 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Libs.Kernel; using SharpEmu.Libs.Kernel;
using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Concurrent; using System.Collections.Concurrent;
@@ -23,6 +24,11 @@ public static class AmprExports
private const uint KernelEventQueueRecordType = 2; private const uint KernelEventQueueRecordType = 2;
private const uint WriteAddressRecordType = 3; private const uint WriteAddressRecordType = 3;
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new(); private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
private static readonly bool _traceAmpr =
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
private static readonly bool _traceAmprReads =
_traceAmpr ||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
private sealed class CommandBufferState private sealed class CommandBufferState
{ {
@@ -75,12 +81,7 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
var buffer = 0UL; if (!InitializeCommandBuffer(ctx, commandBuffer, buffer: 0, size: 0, aux0, aux1, clear: false))
var size = 0UL;
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer);
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size);
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0, aux1, clear: false))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -104,8 +105,9 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
if (!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, 0) || Span<byte> auxiliaryPointers = stackalloc byte[sizeof(ulong) * 2];
!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, 0)) auxiliaryPointers.Clear();
if (!ctx.Memory.TryWrite(commandBuffer + CommandBufferAux0Offset, auxiliaryPointers))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -179,8 +181,15 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
} }
if (!ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out var buffer) || Span<byte> bufferPointers = stackalloc byte[sizeof(ulong) * 2];
!ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out var size) || if (!ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, bufferPointers))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var buffer = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers);
var size = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers[sizeof(ulong)..]);
if (
!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0)) !WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -355,24 +364,26 @@ public static class AmprExports
var commandBuffer = ctx[CpuRegister.Rdi]; var commandBuffer = ctx[CpuRegister.Rdi];
var equeue = ctx[CpuRegister.Rsi]; var equeue = ctx[CpuRegister.Rsi];
var ident = ctx[CpuRegister.Rdx]; var ident = ctx[CpuRegister.Rdx];
var filter = ctx[CpuRegister.Rcx]; var completionToken = ctx[CpuRegister.Rcx];
var userData = ctx[CpuRegister.R8]; var userData = ctx[CpuRegister.R8];
var data = ctx[CpuRegister.R9];
if (commandBuffer == 0) if (commandBuffer == 0)
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
} }
var extra = 0UL; if (!AppendKernelEventQueueRecord(
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out extra); ctx,
commandBuffer,
if (!AppendKernelEventQueueRecord(ctx, commandBuffer, equeue, ident, filter, userData, data, extra)) equeue,
ident,
completionToken,
userData))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; 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; ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -472,20 +483,34 @@ public static class AmprExports
ulong aux1, ulong aux1,
bool clear) bool clear)
{ {
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
if (clear) if (clear)
{ {
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
header.Clear(); header.Clear();
if (!ctx.Memory.TryWrite(commandBuffer, header)) }
else
{
if (!ctx.Memory.TryRead(commandBuffer, header))
{ {
return false; return false;
} }
buffer = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferDataOffset..]);
size = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferSizeOffset..]);
} }
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) && BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSelfOffset..], commandBuffer);
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, aux0) && BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferDataOffset..], buffer);
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, aux1) && BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSizeOffset..], size);
WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0); BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux0Offset..], aux0);
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux1Offset..], aux1);
if (!ctx.Memory.TryWrite(commandBuffer, header))
{
return false;
}
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset: 0);
return true;
} }
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size) private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
@@ -500,6 +525,26 @@ public static class AmprExports
return false; return false;
} }
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset);
return true;
}
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
{
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 3];
BinaryPrimitives.WriteUInt64LittleEndian(pointers, commandBuffer);
BinaryPrimitives.WriteUInt64LittleEndian(pointers[sizeof(ulong)..], buffer);
BinaryPrimitives.WriteUInt64LittleEndian(pointers[(sizeof(ulong) * 2)..], size);
return ctx.Memory.TryWrite(commandBuffer + CommandBufferSelfOffset, pointers);
}
private static void UpdateCommandBufferState(
ulong commandBuffer,
ulong buffer,
ulong size,
ulong writeOffset)
{
var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState()); var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
lock (state) lock (state)
{ {
@@ -507,15 +552,6 @@ public static class AmprExports
state.Size = size; state.Size = size;
state.WriteOffset = writeOffset; state.WriteOffset = writeOffset;
} }
return true;
}
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
{
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
ctx.TryWriteUInt64(commandBuffer + CommandBufferDataOffset, buffer) &&
ctx.TryWriteUInt64(commandBuffer + CommandBufferSizeOffset, size);
} }
private static bool TryGetCommandBufferState( private static bool TryGetCommandBufferState(
@@ -536,9 +572,11 @@ public static class AmprExports
return true; return true;
} }
if (ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer) && Span<byte> pointers = stackalloc byte[sizeof(ulong) * 2];
ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size)) if (ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, pointers))
{ {
buffer = BinaryPrimitives.ReadUInt64LittleEndian(pointers);
size = BinaryPrimitives.ReadUInt64LittleEndian(pointers[sizeof(ulong)..]);
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState()); state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
lock (state) lock (state)
{ {
@@ -591,12 +629,18 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
} }
const int ChunkSize = 64 * 1024; const int ChunkSize = 1024 * 1024;
var buffer = new byte[(int)Math.Min((ulong)ChunkSize, size)]; var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ChunkSize, size));
try try
{ {
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete); using var stream = new FileStream(
hostPath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete,
ChunkSize,
FileOptions.SequentialScan);
if (fileOffset >= (ulong)stream.Length) if (fileOffset >= (ulong)stream.Length)
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -629,6 +673,10 @@ public static class AmprExports
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
} }
finally
{
ArrayPool<byte>.Shared.Return(buffer);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -659,20 +707,17 @@ public static class AmprExports
ulong commandBuffer, ulong commandBuffer,
ulong equeue, ulong equeue,
ulong ident, ulong ident,
ulong filter, ulong completionToken,
ulong userData, ulong userData)
ulong data,
ulong extra)
{ {
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize]; Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
record.Clear(); record.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType); 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[0x08..], equeue);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident); BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData); BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], data); BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], completionToken);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x28..], extra);
return AppendCommandBufferRecord(ctx, commandBuffer, record); return AppendCommandBufferRecord(ctx, commandBuffer, record);
} }
@@ -778,7 +823,7 @@ public static class AmprExports
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1) 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; return;
} }
@@ -800,7 +845,7 @@ public static class AmprExports
string? hostPath, string? hostPath,
int result) int result)
{ {
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal)) if (!_traceAmprReads)
{ {
return; return;
} }
@@ -4,6 +4,7 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text; using System.Text;
using System.Text.Json;
namespace SharpEmu.Libs.AppContent; namespace SharpEmu.Libs.AppContent;
@@ -11,6 +12,8 @@ public static class AppContentExports
{ {
private const ulong BootParamAttrOffset = 4; private const ulong BootParamAttrOffset = 4;
private const string Temp0MountPoint = "/temp0"; private const string Temp0MountPoint = "/temp0";
private const uint AppParamSkuFlag = 0;
private const int AppParamSkuFlagFull = 3;
[SysAbiExport( [SysAbiExport(
Nid = "R9lA82OraNs", Nid = "R9lA82OraNs",
@@ -59,6 +62,41 @@ public static class AppContentExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
Nid = "buYbeLOGWmA", Nid = "buYbeLOGWmA",
ExportName = "sceAppContentTemporaryDataMount2", ExportName = "sceAppContentTemporaryDataMount2",
@@ -83,6 +121,69 @@ public static class AppContentExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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() private static string ResolveTemp0Root()
{ {
const string temp0VariableName = "SHARPEMU_TEMP0_DIR"; 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;
}
}
+179 -190
View File
@@ -48,15 +48,10 @@ public static class FiberExports
[ThreadStatic] [ThreadStatic]
private static ulong _currentFiberAddress; private static ulong _currentFiberAddress;
[ThreadStatic] private static readonly object _fiberGate = new();
private static bool _fiberReturnRequested;
[ThreadStatic]
private static ulong _fiberReturnArgument;
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = 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, FiberStackRange> _stackRanges = new();
private static readonly ConcurrentDictionary<ulong, FiberRunSession> _runSessions = new();
[SysAbiExport( [SysAbiExport(
Nid = "hVYD7Ou2pCQ", Nid = "hVYD7Ou2pCQ",
@@ -146,6 +141,7 @@ public static class FiberExports
} }
_continuations.TryRemove(fiber, out _); _continuations.TryRemove(fiber, out _);
_returnTargets.TryRemove(fiber, out _);
_stackRanges.TryRemove(fiber, out _); _stackRanges.TryRemove(fiber, out _);
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated); _ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
return SetReturn(ctx, 0); return SetReturn(ctx, 0);
@@ -231,43 +227,75 @@ public static class FiberExports
public static int FiberReturnToThread(CpuContext ctx) public static int FiberReturnToThread(CpuContext ctx)
{ {
var fiberAddress = ResolveCurrentFiberAddress(ctx); var fiberAddress = ResolveCurrentFiberAddress(ctx);
var inferredFiber = false;
if (_currentFiberAddress == 0 && fiberAddress != 0)
{
inferredFiber = true;
}
if (fiberAddress == 0) if (fiberAddress == 0)
{ {
return SetReturn(ctx, FiberErrorPermission); 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]; var argOnRunAddress = ctx[CpuRegister.Rsi];
if (argOnRunAddress != 0 && !TryWriteUInt64(ctx, argOnRunAddress, 0)) if (argOnRunAddress != 0 && !TryWriteUInt64(ctx, argOnRunAddress, 0))
{ {
return SetReturn(ctx, FiberErrorInvalid); return SetReturn(ctx, FiberErrorInvalid);
} }
_fiberReturnRequested = true; GuestCpuContinuation transferTarget;
_fiberReturnArgument = ctx[CpuRegister.Rdi]; ulong previousFiber;
if (_runSessions.TryGetValue(fiberAddress, out var session)) lock (_fiberGate)
{
session.SetReturn(ctx[CpuRegister.Rdi]);
}
if (GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{ {
_continuations[fiberAddress] = new FiberContinuation( _continuations[fiberAddress] = new FiberContinuation(
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress), CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
argOnRunAddress); argOnRunAddress);
TraceFiber($"yield{(inferredFiber ? "-inferred" : string.Empty)} fiber=0x{fiberAddress:X16} resume=0x{frame.ReturnRip:X16} rsp=0x{frame.ResumeRsp:X16} return_slot=0x{frame.ReturnSlotAddress:X16} arg_out=0x{argOnRunAddress:X16}");
} if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
else {
{ _continuations.TryRemove(fiberAddress, out _);
TraceFiber($"yield-no-frame{(inferredFiber ? "-inferred" : string.Empty)} fiber=0x{fiberAddress:X16} arg_out=0x{argOnRunAddress:X16}"); 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); return SetReturn(ctx, 0);
} }
@@ -536,170 +564,152 @@ public static class FiberExports
} }
} }
if (fields.State != FiberStateIdle)
{
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
return SetReturn(ctx, FiberErrorState);
}
var previousFiber = ResolveCurrentFiberAddress(ctx); var previousFiber = ResolveCurrentFiberAddress(ctx);
var switchingFromFiber = isSwitch && previousFiber != 0 && previousFiber != fiber; if ((isSwitch && previousFiber == 0) ||
if (isSwitch && previousFiber == 0) (!isSwitch && previousFiber != 0))
{ {
return SetReturn(ctx, FiberErrorPermission); return SetReturn(ctx, FiberErrorPermission);
} }
if (previousFiber == fiber)
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
{
return SetReturn(ctx, FiberErrorPermission);
}
var session = new FiberRunSession();
var ownsSession = _runSessions.TryAdd(fiber, session);
if (!ownsSession && !_runSessions.TryGetValue(fiber, out session))
{ {
return SetReturn(ctx, FiberErrorState); return SetReturn(ctx, FiberErrorState);
} }
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
FiberContinuation? previousContinuation = null; !GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
GuestImportCallFrame switchFrame = default;
if (switchingFromFiber)
{ {
if (!GuestThreadExecution.TryGetCurrentImportCallFrame(out switchFrame)) return SetReturn(ctx, FiberErrorPermission);
}
GuestCpuContinuation transferTarget;
var resumed = false;
lock (_fiberGate)
{
if (!TryReadFiberFields(ctx, fiber, out fields))
{ {
if (ownsSession) return SetReturn(ctx, FiberErrorInvalid);
{ }
_runSessions.TryRemove(fiber, out _); if (fields.State != FiberStateIdle)
} {
TraceFiber($"switch-no-frame from=0x{previousFiber:X16} to=0x{fiber:X16} arg_out=0x{outArgumentAddress:X16}"); TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
return SetReturn(ctx, FiberErrorPermission); return SetReturn(ctx, FiberErrorState);
} }
previousContinuation = new FiberContinuation( FiberContinuation targetContinuation;
CaptureContinuation(ctx, switchFrame.ReturnRip, switchFrame.ResumeRsp, switchFrame.ReturnSlotAddress), 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); outArgumentAddress);
}
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun)) if (previousFiber != 0)
{
if (ownsSession)
{ {
_runSessions.TryRemove(fiber, out _); if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
} previousState != FiberStateRun ||
return SetReturn(ctx, FiberErrorInvalid); !TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
}
if (switchingFromFiber && !TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
{
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle);
if (ownsSession)
{
_runSessions.TryRemove(fiber, out _);
}
return SetReturn(ctx, FiberErrorInvalid);
}
if (previousContinuation.HasValue)
{
_continuations[previousFiber] = previousContinuation.Value;
TraceFiber($"switch-save from=0x{previousFiber:X16} to=0x{fiber:X16} resume=0x{switchFrame.ReturnRip:X16} rsp=0x{switchFrame.ResumeRsp:X16} return_slot=0x{switchFrame.ReturnSlotAddress:X16} arg_out=0x{outArgumentAddress:X16}");
}
var previousReturnRequested = _fiberReturnRequested;
var previousReturnArgument = _fiberReturnArgument;
_currentFiberAddress = fiber;
_fiberReturnRequested = false;
_fiberReturnArgument = 0;
var previousExecutionFiber = GuestThreadExecution.EnterFiber(fiber);
TraceFiber($"run-enter reason={reason} fiber=0x{fiber:X16} prev=0x{previousFiber:X16} thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} managed={Environment.CurrentManagedThreadId} stack=0x{fields.ContextAddress:X16}");
var hasContinuation = _continuations.TryGetValue(fiber, out var continuation);
if (hasContinuation)
{
_continuations.TryRemove(fiber, out _);
}
TraceFiber(hasContinuation
? $"run-dispatch reason={reason} fiber=0x{fiber:X16} resume=1 rip=0x{continuation.Context.Rip:X16} rsp=0x{continuation.Context.Rsp:X16} return_slot=0x{continuation.Context.ReturnSlotAddress:X16} arg_out=0x{continuation.ArgOnRunAddress:X16}"
: $"run-dispatch reason={reason} fiber=0x{fiber:X16} resume=0 rip=0x{fields.Entry:X16} rsp=0x{fields.ContextAddress + fields.ContextSize:X16} arg_out=0x{outArgumentAddress:X16}");
bool callbackOk;
string? callbackError;
try
{
if (hasContinuation)
{
if (continuation.ArgOnRunAddress != 0 &&
!TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argOnRun))
{ {
callbackOk = false; return SetReturn(ctx, FiberErrorState);
callbackError = $"failed to write resumed argOnRun to 0x{continuation.ArgOnRunAddress:X16}";
}
else
{
callbackOk = scheduler.TryCallGuestContinuation(
ctx,
continuation.Context,
reason,
out callbackError);
} }
_continuations[previousFiber] = callerContinuation;
_returnTargets[fiber] = new FiberReturnTarget(previousFiber, null);
} }
else else
{ {
callbackOk = scheduler.TryCallGuestFunction( _returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
ctx,
fields.Entry,
fields.ArgOnInitialize,
argOnRun,
fields.ContextAddress,
fields.ContextSize,
reason,
out callbackError);
} }
}
finally if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
{ {
GuestThreadExecution.RestoreFiber(previousExecutionFiber); 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; _currentFiberAddress = fiber;
var returnArgument = _fiberReturnArgument; _ = GuestThreadExecution.EnterFiber(fiber);
if (!returnRequested && session.TryGetReturn(out var sessionReturnArgument)) GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
{ TraceFiber(
returnRequested = true; $"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
returnArgument = sessionReturnArgument; $"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
}
if (ownsSession)
{
_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}");
return SetReturn(ctx, 0); return SetReturn(ctx, 0);
} }
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( private static GuestCpuContinuation CaptureContinuation(
CpuContext ctx, CpuContext ctx,
ulong resumeRip, ulong resumeRip,
@@ -1007,30 +1017,9 @@ public static class FiberExports
GuestCpuContinuation Context, GuestCpuContinuation Context,
ulong ArgOnRunAddress); ulong ArgOnRunAddress);
private sealed class FiberRunSession private readonly record struct FiberReturnTarget(
{ ulong PreviousFiber,
private int _returnRequested; FiberContinuation? ThreadContinuation);
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 FiberStackRange(ulong Start, ulong Size) private readonly record struct FiberStackRange(ulong Start, ulong Size)
{ {
@@ -11,8 +11,13 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelAprCompatExports 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 _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( [SysAbiExport(
Nid = "ASoW5WE-UPo", Nid = "ASoW5WE-UPo",
@@ -37,7 +42,7 @@ public static class KernelAprCompatExports
submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
} }
_submittedCommandBuffers[submissionId] = commandBuffer; _submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, priority, resultAddress);
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer); var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
@@ -67,20 +72,23 @@ public static class KernelAprCompatExports
public static int KernelAprWaitCommandBuffer(CpuContext ctx) public static int KernelAprWaitCommandBuffer(CpuContext ctx)
{ {
var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]); var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]);
var priority = ctx[CpuRegister.Rsi]; var waitArg1 = ctx[CpuRegister.Rsi];
var resultAddress = ctx[CpuRegister.Rdx]; 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; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
} }
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress);
if (resultAddress != 0 && !TryWriteAprResult(ctx, 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; 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; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -98,7 +106,7 @@ public static class KernelAprCompatExports
} }
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); 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); var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
@@ -125,7 +133,7 @@ public static class KernelAprCompatExports
} }
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId)); 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); var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK) if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
@@ -149,6 +157,27 @@ public static class KernelAprCompatExports
return ctx.Memory.TryWrite(resultAddress, result); 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) private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{ {
Span<byte> buffer = stackalloc byte[sizeof(uint)]; Span<byte> buffer = stackalloc byte[sizeof(uint)];
@@ -164,7 +193,7 @@ public static class KernelAprCompatExports
ulong priority, ulong priority,
ulong aux) ulong aux)
{ {
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal)) if (!_traceApr)
{ {
return; return;
} }
@@ -181,4 +210,46 @@ public static class KernelAprCompatExports
$"[LOADER][TRACE] apr.{operation}.result: addr=0x{aux:X16} q0=0x{result0:X16} q1=0x{result1:X16}"); $"[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}");
}
} }
@@ -127,6 +127,7 @@ public static class KernelEventFlagCompatExports
TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip: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); return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
} }
@@ -226,7 +227,7 @@ public static class KernelEventFlagCompatExports
Monitor.Enter(state.Gate); Monitor.Enter(state.Gate);
try try
{ {
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, out var immediateWaitResult)) if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
{ {
return SetReturn(ctx, immediateWaitResult); return SetReturn(ctx, immediateWaitResult);
} }
@@ -242,7 +243,28 @@ public static class KernelEventFlagCompatExports
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle; var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx); var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
var managedThread = Environment.CurrentManagedThreadId; var managedThread = Environment.CurrentManagedThreadId;
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEventFlag"); 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-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)}"); TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
if (!requestedBlock) if (!requestedBlock)
@@ -270,7 +292,7 @@ public static class KernelEventFlagCompatExports
Monitor.Enter(state.Gate); 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); state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
releaseWaiter = false; releaseWaiter = false;
@@ -369,19 +391,21 @@ public static class KernelEventFlagCompatExports
} }
private static bool TryCompleteSatisfiedWait( private static bool TryCompleteSatisfiedWait(
CpuContext ctx, CpuContext ctx,
EventFlagState state, EventFlagState state,
ulong pattern, ulong pattern,
uint waitMode, uint waitMode,
out OrbisGen2Result result) ulong resultAddress,
out OrbisGen2Result result)
{ {
result = OrbisGen2Result.ORBIS_GEN2_OK; result = OrbisGen2Result.ORBIS_GEN2_OK;
if (!IsSatisfied(state.Bits, pattern, waitMode)) if (!IsSatisfied(state.Bits, pattern, waitMode))
{ {
return false; return false;
} }
if (!TryWriteResultPattern(ctx, ctx[CpuRegister.Rcx], state.Bits)) if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{ {
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
return true; return true;
@@ -391,6 +415,41 @@ public static class KernelEventFlagCompatExports
return true; 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) => private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
address == 0 || ctx.TryWriteUInt64(address, bits); address == 0 || ctx.TryWriteUInt64(address, bits);
@@ -10,10 +10,14 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelEventQueueCompatExports public static class KernelEventQueueCompatExports
{ {
private const int KernelEventSize = 0x20; 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 object _eventQueueGate = new();
private static readonly HashSet<ulong> _eventQueues = new(); private static readonly HashSet<ulong> _eventQueues = new();
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = 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; private static long _nextEventQueueHandle = 1;
public readonly record struct KernelQueuedEvent( public readonly record struct KernelQueuedEvent(
@@ -24,6 +28,11 @@ public static class KernelEventQueueCompatExports
ulong Data, ulong Data,
ulong UserData); ulong UserData);
private readonly record struct KernelEventRegistration(
ulong Ident,
short Filter,
ulong UserData);
[SysAbiExport( [SysAbiExport(
Nid = "D0OdFMjp46I", Nid = "D0OdFMjp46I",
ExportName = "sceKernelCreateEqueue", ExportName = "sceKernelCreateEqueue",
@@ -42,6 +51,7 @@ public static class KernelEventQueueCompatExports
{ {
_eventQueues.Add(handle); _eventQueues.Add(handle);
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>(); _pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
} }
if (!ctx.TryWriteUInt64(outAddress, handle)) if (!ctx.TryWriteUInt64(outAddress, handle))
@@ -65,6 +75,7 @@ public static class KernelEventQueueCompatExports
{ {
_eventQueues.Remove(handle); _eventQueues.Remove(handle);
_pendingEvents.Remove(handle); _pendingEvents.Remove(handle);
_registeredEvents.Remove(handle);
} }
TraceEventQueue(ctx, "delete", handle); TraceEventQueue(ctx, "delete", handle);
@@ -122,8 +133,16 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int KernelAddAmprEvent(CpuContext ctx) public static int KernelAddAmprEvent(CpuContext ctx)
{ {
TraceEventQueue(ctx, "add_ampr", ctx[CpuRegister.Rdi]); var handle = ctx[CpuRegister.Rdi];
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
@@ -133,8 +152,16 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int KernelAddAmprSystemEvent(CpuContext ctx) public static int KernelAddAmprSystemEvent(CpuContext ctx)
{ {
TraceEventQueue(ctx, "add_ampr_system", ctx[CpuRegister.Rdi]); var handle = ctx[CpuRegister.Rdi];
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
@@ -144,8 +171,15 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int KernelDeleteAmprEvent(CpuContext ctx) public static int KernelDeleteAmprEvent(CpuContext ctx)
{ {
TraceEventQueue(ctx, "delete_ampr", ctx[CpuRegister.Rdi]); var handle = ctx[CpuRegister.Rdi];
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
@@ -155,8 +189,15 @@ public static class KernelEventQueueCompatExports
LibraryName = "libKernel")] LibraryName = "libKernel")]
public static int KernelDeleteAmprSystemEvent(CpuContext ctx) public static int KernelDeleteAmprSystemEvent(CpuContext ctx)
{ {
TraceEventQueue(ctx, "delete_ampr_system", ctx[CpuRegister.Rdi]); var handle = ctx[CpuRegister.Rdi];
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
@@ -171,6 +212,57 @@ public static class KernelEventQueueCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
Nid = "fzyMKs9kim0", Nid = "fzyMKs9kim0",
ExportName = "sceKernelWaitEqueue", ExportName = "sceKernelWaitEqueue",
@@ -196,7 +288,13 @@ public static class KernelEventQueueCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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); TraceEventQueue(ctx, "wait-block", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -216,6 +314,7 @@ public static class KernelEventQueueCompatExports
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent) public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
{ {
var queued = false;
lock (_eventQueueGate) lock (_eventQueueGate)
{ {
if (!_eventQueues.Contains(handle)) if (!_eventQueues.Contains(handle))
@@ -230,10 +329,100 @@ public static class KernelEventQueueCompatExports
} }
queue.AddLast(queuedEvent); 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; 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( public static bool TriggerDisplayEvent(
ulong handle, ulong handle,
ulong ident, ulong ident,
@@ -241,6 +430,7 @@ public static class KernelEventQueueCompatExports
ulong eventHint, ulong eventHint,
ulong userData) ulong userData)
{ {
var triggered = false;
lock (_eventQueueGate) lock (_eventQueueGate)
{ {
if (!_eventQueues.Contains(handle)) if (!_eventQueues.Contains(handle))
@@ -254,17 +444,8 @@ public static class KernelEventQueueCompatExports
_pendingEvents[handle] = events; _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 count = 1UL;
var pendingNode = FindPendingEvent(events, ident, filter);
if (pendingNode is not null) if (pendingNode is not null)
{ {
count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL); count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL);
@@ -289,8 +470,80 @@ public static class KernelEventQueueCompatExports
events.AddLast(triggeredEvent); 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) private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
+71 -6
View File
@@ -204,6 +204,16 @@ public static class KernelExports
var nameAddress = ctx[CpuRegister.R8]; var nameAddress = ctx[CpuRegister.R8];
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256); var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
var threadHandle = KernelPthreadState.CreateThreadHandle(name); var threadHandle = KernelPthreadState.CreateThreadHandle(name);
KernelPthreadExtendedCompatExports.GetThreadStartScheduling(
ctx,
attrAddress,
out var priority,
out var affinityMask);
KernelPthreadExtendedCompatExports.RegisterThreadStart(
threadHandle,
name,
priority,
affinityMask);
if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, threadHandle)) if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, threadHandle))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -212,13 +222,22 @@ public static class KernelExports
if (ShouldTracePthread()) if (ShouldTracePthread())
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} name='{name}' -> thread=0x{threadHandle:X16}"); $"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} " +
$"entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} " +
$"name='{name}' priority={priority} affinity=0x{affinityMask:X} -> thread=0x{threadHandle:X16}");
} }
var scheduler = GuestThreadExecution.Scheduler; var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is not null && entryAddress != 0) if (scheduler is not null && entryAddress != 0)
{ {
var request = new GuestThreadStartRequest(threadHandle, entryAddress, argument, attrAddress, name); var request = new GuestThreadStartRequest(
threadHandle,
entryAddress,
argument,
attrAddress,
name,
priority,
affinityMask);
if (!scheduler.TryStartThread(ctx, request, out var error)) if (!scheduler.TryStartThread(ctx, request, out var error))
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
@@ -252,6 +271,32 @@ public static class KernelExports
return PthreadCreate(ctx); 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( [SysAbiExport(
Nid = "onNY9Byn-W8", Nid = "onNY9Byn-W8",
ExportName = "scePthreadJoin", ExportName = "scePthreadJoin",
@@ -261,10 +306,6 @@ public static class KernelExports
{ {
var threadId = ctx[CpuRegister.Rdi]; var threadId = ctx[CpuRegister.Rdi];
var returnValueAddress = ctx[CpuRegister.Rsi]; var returnValueAddress = ctx[CpuRegister.Rsi];
if (returnValueAddress != 0 && !ctx.TryWriteUInt64(returnValueAddress, 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (ShouldTracePthread()) if (ShouldTracePthread())
{ {
@@ -272,6 +313,30 @@ public static class KernelExports
$"[LOADER][TRACE] pthread_join: thread=0x{threadId:X16} retval_out=0x{returnValueAddress:X16}"); $"[LOADER][TRACE] pthread_join: thread=0x{threadId:X16} retval_out=0x{returnValueAddress:X16}");
} }
var returnValue = 0UL;
if (GuestThreadExecution.Scheduler is { } scheduler &&
!scheduler.TryJoinThread(ctx, threadId, out returnValue, out var error))
{
Console.Error.WriteLine(
$"[LOADER][ERROR] pthread_join: thread=0x{threadId:X16}: {error}");
var result = string.Equals(
error,
"thread cannot join itself",
StringComparison.Ordinal)
? OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
: OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
if (returnValueAddress != 0 &&
!ctx.TryWriteUInt64(returnValueAddress, returnValue))
{
ctx[CpuRegister.Rax] =
unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0; ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
File diff suppressed because it is too large Load Diff
@@ -1,7 +1,9 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading; using System.Threading;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@@ -9,7 +11,6 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelPthreadCompatExports public static class KernelPthreadCompatExports
{ {
private const int MutexTypeDefault = 1;
private const int MutexTypeErrorCheck = 1; private const int MutexTypeErrorCheck = 1;
private const int MutexTypeRecursive = 2; private const int MutexTypeRecursive = 2;
private const int MutexTypeNormal = 3; private const int MutexTypeNormal = 3;
@@ -19,22 +20,39 @@ public static class KernelPthreadCompatExports
private const int MutexAttrObjectSize = 0x40; private const int MutexAttrObjectSize = 0x40;
private const int CondObjectSize = 0x100; private const int CondObjectSize = 0x100;
private const int DefaultSpuriousCondWakeMilliseconds = 1; 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 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, PthreadMutexAttrState> _mutexAttrStates = new();
private static readonly Dictionary<ulong, PthreadCondState> _condStates = 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 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 private sealed class PthreadMutexState
{ {
public SemaphoreSlim Semaphore { get; } = new(1, 1); public SemaphoreSlim Semaphore { get; } = new(1, 1);
public ulong OwnerThreadId { get; set; } public ulong OwnerThreadId { get; set; }
public int RecursionCount { get; set; } public int RecursionCount { get; set; }
public int Type { get; set; } = MutexTypeDefault; public int Type { get; set; } = MutexTypeErrorCheck;
public int Protocol { get; set; } public int Protocol { get; set; }
} }
private sealed class PthreadMutexWaiter
{
public required ulong ThreadId { get; init; }
public int Reserved;
}
private sealed class PthreadCondState private sealed class PthreadCondState
{ {
public object SyncRoot { get; } = new(); public object SyncRoot { get; } = new();
@@ -57,6 +75,13 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
Nid = "3PtV6p3QNX4", Nid = "3PtV6p3QNX4",
ExportName = "scePthreadEqual", ExportName = "scePthreadEqual",
@@ -340,6 +365,93 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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) private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
{ {
if (mutexAddress == 0) if (mutexAddress == 0)
@@ -364,19 +476,13 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; 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.TryRemove(mutexAddress, out _);
{ _mutexStates.TryRemove(handle, out _);
_mutexStates.Remove(mutexAddress);
_mutexStates.Remove(handle);
}
state.Semaphore.Dispose(); state.Semaphore.Dispose();
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -393,15 +499,10 @@ public static class KernelPthreadCompatExports
} }
var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress); var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress);
PthreadMutexState? state; _mutexStates.TryRemove(resolvedAddress, out var state);
lock (_stateGate) if (resolvedAddress != mutexAddress)
{ {
_mutexStates.TryGetValue(resolvedAddress, out state); _mutexStates.TryRemove(mutexAddress, out _);
_mutexStates.Remove(resolvedAddress);
if (resolvedAddress != mutexAddress)
{
_mutexStates.Remove(mutexAddress);
}
} }
if (state is null) if (state is null)
@@ -409,7 +510,7 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
} }
_ = ctx.TryWriteUInt64(mutexAddress, 0); _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
state.Semaphore.Dispose(); state.Semaphore.Dispose();
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -464,15 +565,33 @@ public static class KernelPthreadCompatExports
} }
} }
var acquired = true; var acquired = state.Semaphore.Wait(0);
if (tryOnly) if (!acquired)
{ {
acquired = state.Semaphore.Wait(0); // Guest-thread blocking for pthread_mutex_lock is currently disabled.
} // Demon's Souls deadlocks during PS5SyncEvent initialization.
else /* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
{ if (!tryOnly &&
state.Semaphore.Wait(); 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) if (!acquired)
{ {
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY); TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
@@ -531,6 +650,7 @@ public static class KernelPthreadCompatExports
try try
{ {
state.Semaphore.Release(); state.Semaphore.Release();
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
} }
catch (SemaphoreFullException) catch (SemaphoreFullException)
{ {
@@ -555,7 +675,7 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
var initialState = new PthreadMutexAttrState(MutexTypeDefault, 0); var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
if (!WriteMutexAttrObject(ctx, handle, initialState)) if (!WriteMutexAttrObject(ctx, handle, initialState))
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -567,7 +687,7 @@ public static class KernelPthreadCompatExports
_mutexAttrStates[handle] = initialState; _mutexAttrStates[handle] = initialState;
} }
if (!ctx.TryWriteUInt64(attrAddress, handle)) if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, handle))
{ {
lock (_stateGate) lock (_stateGate)
{ {
@@ -614,7 +734,7 @@ public static class KernelPthreadCompatExports
{ {
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state)) if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
{ {
state = new PthreadMutexAttrState(MutexTypeDefault, 0); state = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
} }
updatedState = state with { Type = NormalizeMutexType(type) }; updatedState = state with { Type = NormalizeMutexType(type) };
@@ -643,7 +763,7 @@ public static class KernelPthreadCompatExports
{ {
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state)) if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
{ {
state = new PthreadMutexAttrState(MutexTypeDefault, 0); state = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
} }
updatedState = state with { Protocol = protocol }; updatedState = state with { Protocol = protocol };
@@ -666,22 +786,16 @@ public static class KernelPthreadCompatExports
return 0; 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 +811,13 @@ public static class KernelPthreadCompatExports
return false; 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; return false;
} }
@@ -718,14 +829,11 @@ public static class KernelPthreadCompatExports
if (pointedHandle != 0) if (pointedHandle != 0)
{ {
lock (_stateGate) if (_mutexStates.TryGetValue(pointedHandle, out state))
{ {
if (_mutexStates.TryGetValue(pointedHandle, out state)) _mutexStates.TryAdd(mutexAddress, state);
{ resolvedAddress = pointedHandle;
_mutexStates[mutexAddress] = state; return true;
resolvedAddress = pointedHandle;
return true;
}
} }
resolvedAddress = pointedHandle; resolvedAddress = pointedHandle;
@@ -738,7 +846,7 @@ public static class KernelPthreadCompatExports
return false; return false;
} }
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeDefault, out resolvedAddress, out state); return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeErrorCheck, out resolvedAddress, out state);
} }
private static ulong ResolveMutexAttrHandle(CpuContext ctx, ulong attrAddress) private static ulong ResolveMutexAttrHandle(CpuContext ctx, ulong attrAddress)
@@ -748,7 +856,7 @@ public static class KernelPthreadCompatExports
return 0; return 0;
} }
if (ctx.TryReadUInt64(attrAddress, out var pointedHandle) && pointedHandle != 0) if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, attrAddress, out var pointedHandle) && pointedHandle != 0)
{ {
lock (_stateGate) lock (_stateGate)
{ {
@@ -782,7 +890,7 @@ public static class KernelPthreadCompatExports
{ {
return _mutexAttrStates.TryGetValue(resolvedAddress, out var state) return _mutexAttrStates.TryGetValue(resolvedAddress, out var state)
? state ? state
: new PthreadMutexAttrState(MutexTypeDefault, 0); : new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
} }
} }
@@ -801,7 +909,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) lock (_stateGate)
{ {
@@ -833,7 +941,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; return false;
} }
@@ -872,7 +980,7 @@ public static class KernelPthreadCompatExports
_condStates[handle] = createdState; _condStates[handle] = createdState;
} }
if (!ctx.TryWriteUInt64(condAddress, handle)) if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, handle))
{ {
lock (_stateGate) lock (_stateGate)
{ {
@@ -936,7 +1044,7 @@ public static class KernelPthreadCompatExports
_condStates[handle] = state; _condStates[handle] = state;
} }
if (!ctx.TryWriteUInt64(condAddress, handle)) if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, handle))
{ {
lock (_stateGate) lock (_stateGate)
{ {
@@ -967,7 +1075,7 @@ public static class KernelPthreadCompatExports
} }
} }
_ = ctx.TryWriteUInt64(condAddress, 0); _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -1103,6 +1211,63 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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) private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
{ {
if (timeoutUsec == 0) if (timeoutUsec == 0)
@@ -1127,15 +1292,55 @@ public static class KernelPthreadCompatExports
{ {
return type switch return type switch
{ {
0 => MutexTypeDefault, 0 => MutexTypeErrorCheck,
1 => MutexTypeErrorCheck, 1 => MutexTypeErrorCheck,
2 => MutexTypeRecursive, 2 => MutexTypeRecursive,
3 => MutexTypeNormal, 3 => MutexTypeNormal,
4 => MutexTypeAdaptiveNp, 4 => MutexTypeAdaptiveNp,
_ => MutexTypeDefault, _ => MutexTypeErrorCheck,
}; };
} }
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) private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
{ {
var createdState = new PthreadMutexState var createdState = new PthreadMutexState
@@ -1174,13 +1379,10 @@ public static class KernelPthreadCompatExports
_mutexStates[handle] = createdState; _mutexStates[handle] = createdState;
} }
if (!ctx.TryWriteUInt64(mutexAddress, handle)) if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, handle))
{ {
lock (_stateGate) _mutexStates.TryRemove(mutexAddress, out _);
{ _mutexStates.TryRemove(handle, out _);
_mutexStates.Remove(mutexAddress);
_mutexStates.Remove(handle);
}
resolvedAddress = 0; resolvedAddress = 0;
state = null; state = null;
@@ -1204,15 +1406,27 @@ public static class KernelPthreadCompatExports
$"[LOADER][TRACE] pthread_self: stale_rdi=0x{ctx[CpuRegister.Rdi]:X16} thread=0x{currentThreadHandle:X16} tid=0x{currentThreadId:X16}"); $"[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()) if (!ShouldTracePthread())
{ {
return; return;
} }
_ = ctx.TryReadUInt64(mutexAddress, out var guestWord0); var suffix = string.IsNullOrWhiteSpace(error) ? string.Empty : $" error={error}";
_ = ctx.TryReadUInt64(mutexAddress + 8, out var guestWord1); 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( Console.Error.WriteLine(
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " + $"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " + $"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
@@ -1222,7 +1436,7 @@ public static class KernelPthreadCompatExports
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result) private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
{ {
if (!ShouldTracePthread()) if (!_tracePthreadConds)
{ {
return; return;
} }
@@ -1234,6 +1448,45 @@ public static class KernelPthreadCompatExports
private static bool ShouldTracePthread() 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 SharpEmu.HLE;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
@@ -12,13 +13,13 @@ namespace SharpEmu.Libs.Kernel;
public static class KernelPthreadExtendedCompatExports public static class KernelPthreadExtendedCompatExports
{ {
private const int DefaultThreadPriority = 700; private const int DefaultThreadPriority = 700;
private const ulong DefaultThreadAffinityMask = ulong.MaxValue; private const ulong DefaultThreadAffinityMask = 0x7FUL;
private const int DefaultDetachState = 0; private const int DefaultDetachState = 0;
private const ulong DefaultGuardSize = 0x1000UL; private const ulong DefaultGuardSize = 0x1000UL;
private const ulong DefaultStackSize = 0x1_00000UL; private const ulong DefaultStackSize = 0x1_00000UL;
private const int DefaultInheritSched = 0; private const int DefaultInheritSched = 4;
private const int DefaultSchedPolicy = 0; private const int DefaultSchedPolicy = 1;
private const int DefaultSchedPriority = 0; private const int DefaultSchedPriority = DefaultThreadPriority;
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000; private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_0000_0000; private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_0000_0000;
@@ -26,12 +27,54 @@ public static class KernelPthreadExtendedCompatExports
private static readonly Dictionary<ulong, ThreadState> _threadStates = new(); private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
private static readonly Dictionary<ulong, PthreadAttrState> _attrStates = new(); private static readonly Dictionary<ulong, PthreadAttrState> _attrStates = new();
private static readonly Dictionary<ulong, PthreadRwlockState> _rwlockStates = 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 int _nextTlsKey = 1;
private static long _nextSyntheticRwlockHandleId = 1; private static long _nextSyntheticRwlockHandleId = 1;
private static long _nextSyntheticPthreadAttrHandleId = 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();
internal static void GetThreadStartScheduling(
CpuContext ctx,
ulong attrAddress,
out int priority,
out ulong affinityMask)
{
if (attrAddress == 0)
{
priority = DefaultThreadPriority;
affinityMask = DefaultThreadAffinityMask;
return;
}
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
lock (_stateGate)
{
var attributes = GetOrCreateAttrStateLocked(resolvedAddress);
priority = attributes.SchedPriority;
affinityMask = attributes.AffinityMask;
}
}
internal static void RegisterThreadStart(
ulong thread,
string name,
int priority,
ulong affinityMask)
{
lock (_stateGate)
{
var state = GetOrCreateThreadStateLocked(thread);
state.Name = name;
state.Priority = priority;
state.AffinityMask = affinityMask;
state.Attributes = state.Attributes with
{
SchedPriority = priority,
AffinityMask = affinityMask,
};
}
}
private sealed class ThreadState private sealed class ThreadState
{ {
@@ -185,6 +228,35 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
Nid = "1tKyG7RlMJo", Nid = "1tKyG7RlMJo",
ExportName = "scePthreadGetprio", ExportName = "scePthreadGetprio",
@@ -764,7 +836,7 @@ public static class KernelPthreadExtendedCompatExports
_rwlockStates[syntheticHandle] = rwlock; _rwlockStates[syntheticHandle] = rwlock;
} }
_ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle); _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle);
ctx[CpuRegister.Rax] = 0; ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -818,7 +890,7 @@ public static class KernelPthreadExtendedCompatExports
} }
} }
_ = ctx.TryWriteUInt64(rwlockAddress, 0); _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, 0);
ctx[CpuRegister.Rax] = 0; ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -930,15 +1002,13 @@ public static class KernelPthreadExtendedCompatExports
} }
int key; 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)) if (!TryWriteInt32(ctx, outKeyAddress, key))
@@ -965,17 +1035,14 @@ public static class KernelPthreadExtendedCompatExports
public static int PosixPthreadKeyDelete(CpuContext ctx) public static int PosixPthreadKeyDelete(CpuContext ctx)
{ {
var key = unchecked((int)ctx[CpuRegister.Rdi]); 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) foreach (var values in _threadLocalSpecific.Values)
{ {
entry.Value.Remove(key); values.TryRemove(key, out _);
}
} }
ctx[CpuRegister.Rax] = 0; ctx[CpuRegister.Rax] = 0;
@@ -999,22 +1066,15 @@ public static class KernelPthreadExtendedCompatExports
var key = unchecked((int)ctx[CpuRegister.Rdi]); var key = unchecked((int)ctx[CpuRegister.Rdi]);
var value = ctx[CpuRegister.Rsi]; var value = ctx[CpuRegister.Rsi];
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle(); var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
lock (_stateGate) if (!_tlsKeys.ContainsKey(key))
{ {
if (!_tlsKeys.TryGetValue(key, out _)) return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
{
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;
} }
var values = _threadLocalSpecific.GetOrAdd(
currentThreadHandle,
static _ => new ConcurrentDictionary<int, ulong>());
values[key] = value;
ctx[CpuRegister.Rax] = 0; ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -1036,19 +1096,16 @@ public static class KernelPthreadExtendedCompatExports
var key = unchecked((int)ctx[CpuRegister.Rdi]); var key = unchecked((int)ctx[CpuRegister.Rdi]);
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle(); var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
ulong value = 0; 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) && if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) &&
values.TryGetValue(key, out var storedValue)) values.TryGetValue(key, out var storedValue))
{ {
value = storedValue; value = storedValue;
}
} }
ctx[CpuRegister.Rax] = value; ctx[CpuRegister.Rax] = value;
@@ -1134,7 +1191,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) lock (_stateGate)
{ {
@@ -1166,7 +1223,7 @@ public static class KernelPthreadExtendedCompatExports
} }
} }
if (!ctx.TryReadUInt64(rwlockAddress, out var pointedHandle)) if (!KernelMemoryCompatExports.TryReadUInt64Compat(ctx, rwlockAddress, out var pointedHandle))
{ {
return false; return false;
} }
@@ -1201,7 +1258,7 @@ public static class KernelPthreadExtendedCompatExports
_rwlockStates[syntheticHandle] = createdRwlock; _rwlockStates[syntheticHandle] = createdRwlock;
} }
_ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle); _ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle);
resolvedAddress = syntheticHandle; resolvedAddress = syntheticHandle;
rwlock = createdRwlock; rwlock = createdRwlock;
return true; return true;
+4 -10
View File
@@ -1,6 +1,7 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using SharpEmu.HLE; using SharpEmu.HLE;
@@ -11,8 +12,7 @@ internal static class KernelPthreadState
{ {
private const int ThreadObjectSize = 0x1000; private const int ThreadObjectSize = 0x1000;
private static readonly object Gate = new(); private static readonly ConcurrentDictionary<ulong, ThreadIdentity> Threads = new();
private static readonly Dictionary<ulong, ThreadIdentity> Threads = new();
private static readonly byte[] ZeroThreadObject = new byte[ThreadObjectSize]; private static readonly byte[] ZeroThreadObject = new byte[ThreadObjectSize];
private static long _nextUniqueThreadId = 1; private static long _nextUniqueThreadId = 1;
@@ -56,10 +56,7 @@ internal static class KernelPthreadState
internal static bool TryGetThreadIdentity(ulong threadHandle, out ThreadIdentity identity) 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() private static void EnsureCurrentThreadRegistered()
@@ -81,10 +78,7 @@ internal static class KernelPthreadState
Marshal.Copy(ZeroThreadObject, 0, pointer, ThreadObjectSize); Marshal.Copy(ZeroThreadObject, 0, pointer, ThreadObjectSize);
var handle = unchecked((ulong)pointer.ToInt64()); 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; return handle;
} }
@@ -57,6 +57,13 @@ public static class KernelRuntimeCompatExports
private static readonly (ulong Base, ulong Size)[] _prtApertures = new (ulong Base, ulong Size)[3]; private static readonly (ulong Base, ulong Size)[] _prtApertures = new (ulong Base, ulong Size)[3];
private static int _stackChkFailCount; private static int _stackChkFailCount;
private static long _usleepTraceCount; 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)] [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
private delegate ulong RdtscDelegate(); private delegate ulong RdtscDelegate();
@@ -80,10 +87,20 @@ public static class KernelRuntimeCompatExports
if (micros < 1000) 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 else
{ {
_shortUsleepCount = 0;
var sleepMilliseconds = (int)Math.Min((micros + 999UL) / 1000UL, int.MaxValue); var sleepMilliseconds = (int)Math.Min((micros + 999UL) / 1000UL, int.MaxValue);
Thread.Sleep(sleepMilliseconds); Thread.Sleep(sleepMilliseconds);
} }
@@ -94,7 +111,7 @@ public static class KernelRuntimeCompatExports
private static void TraceUsleepSpin(CpuContext ctx, ulong micros) private static void TraceUsleepSpin(CpuContext ctx, ulong micros)
{ {
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal)) if (!_traceUsleep)
{ {
return; return;
} }
@@ -106,6 +123,8 @@ public static class KernelRuntimeCompatExports
} }
var rbx = ctx[CpuRegister.Rbx]; var rbx = ctx[CpuRegister.Rbx];
var r12 = ctx[CpuRegister.R12];
var r13 = ctx[CpuRegister.R13];
var lockAddress = rbx == 0 ? 0 : rbx + 0xF78; var lockAddress = rbx == 0 ? 0 : rbx + 0xF78;
var lockText = "unreadable"; var lockText = "unreadable";
if (lockAddress != 0 && ctx.TryReadUInt64(lockAddress, out var lockValue)) if (lockAddress != 0 && ctx.TryReadUInt64(lockAddress, out var lockValue))
@@ -113,6 +132,25 @@ public static class KernelRuntimeCompatExports
lockText = $"0x{lockValue:X16}"; 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) var returnRip = GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)
? frame.ReturnRip ? frame.ReturnRip
: 0UL; : 0UL;
@@ -120,7 +158,20 @@ public static class KernelRuntimeCompatExports
var fiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx); var fiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][TRACE] usleep#{count}: usec={micros} ret=0x{returnRip:X16} thread=0x{thread:X16} fiber=0x{fiber:X16} 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( [SysAbiExport(
@@ -560,19 +611,16 @@ public static class KernelRuntimeCompatExports
public static int ErrorAddress(CpuContext ctx) public static int ErrorAddress(CpuContext ctx)
{ {
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset); 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; ctx[CpuRegister.Rax] = address;
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
Nid = "bnZxYgAFeA0", Nid = "bnZxYgAFeA0",
ExportName = "sceKernelGetSanitizerNewReplaceExternal", ExportName = "sceKernelGetSanitizerNewReplaceExternal",
@@ -1484,30 +1532,40 @@ public static class KernelRuntimeCompatExports
ulong.TryParse(overrideHzText, out var overrideHz) && ulong.TryParse(overrideHzText, out var overrideHz) &&
overrideHz >= minSane) overrideHz >= minSane)
{ {
TraceKernelTscFrequency("env", overrideHz);
return overrideHz; return overrideHz;
} }
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
{
return cpuidHz;
}
if (TryCalibrateHostTscFrequency(out ulong calibratedHz) && calibratedHz >= minSane) if (TryCalibrateHostTscFrequency(out ulong calibratedHz) && calibratedHz >= minSane)
{ {
TraceKernelTscFrequency("calibrated-rdtsc", calibratedHz);
return calibratedHz; return calibratedHz;
} }
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
{
TraceKernelTscFrequency("cpuid", cpuidHz);
return cpuidHz;
}
var hostQpc = Stopwatch.Frequency > 0 var hostQpc = Stopwatch.Frequency > 0
? unchecked((ulong)Stopwatch.Frequency) ? unchecked((ulong)Stopwatch.Frequency)
: DefaultKernelTscFrequency; : DefaultKernelTscFrequency;
if (hostQpc >= minSane) if (hostQpc >= minSane)
{ {
TraceKernelTscFrequency("qpc", hostQpc);
return hostQpc; return hostQpc;
} }
TraceKernelTscFrequency("default", DefaultKernelTscFrequency);
return 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) private static bool TryResolveCpuidTscFrequency(out ulong frequencyHz)
{ {
frequencyHz = 0; frequencyHz = 0;
@@ -1767,7 +1825,7 @@ public static class KernelRuntimeCompatExports
} }
var invokeArgs = allocateAtHasAllowAlternativeArg var invokeArgs = allocateAtHasAllowAlternativeArg
? new object[] { desiredAddress, length, false, false } ? new object[] { desiredAddress, length, false, allowSearch }
: new object[] { desiredAddress, length, false }; : new object[] { desiredAddress, length, false };
var result = allocateAt.Invoke(memoryObject, invokeArgs); var result = allocateAt.Invoke(memoryObject, invokeArgs);
if (result is not ulong allocated || allocated == 0) 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); 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}"); 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); 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 // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.Network; namespace SharpEmu.Libs.Network;
public static class NetCtlExports 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( [SysAbiExport(
Nid = "gky0+oaNM4k", Nid = "gky0+oaNM4k",
ExportName = "sceNetCtlInit", ExportName = "sceNetCtlInit",
@@ -17,4 +46,184 @@ public static class NetCtlExports
ctx[CpuRegister.Rax] = 0; ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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 // SPDX-License-Identifier: GPL-2.0-or-later
using System; using System;
using System.Buffers.Binary;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
@@ -16,11 +17,15 @@ public static class NetExports
private const int MaxNameLength = 256; private const int MaxNameLength = 256;
private static readonly ConcurrentDictionary<int, NetPool> _pools = new(); private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
private static int _nextPoolId; private static int _nextPoolId;
private static int _nextResolverId = 0x2000;
private static bool _initialized; private static bool _initialized;
private sealed record NetPool(string Name, int Size, int Flags); private sealed record NetPool(string Name, int Size, int Flags);
private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError);
[SysAbiExport( [SysAbiExport(
Nid = "Nlev7Lg8k3A", Nid = "Nlev7Lg8k3A",
ExportName = "sceNetInit", ExportName = "sceNetInit",
@@ -42,6 +47,7 @@ public static class NetExports
{ {
_initialized = false; _initialized = false;
_pools.Clear(); _pools.Clear();
_resolvers.Clear();
TraceNet("term", 0, 0, 0, 0); TraceNet("term", 0, 0, 0, 0);
return SetReturn(ctx, 0); return SetReturn(ctx, 0);
} }
@@ -91,6 +97,70 @@ public static class NetExports
return SetReturn(ctx, 0); 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) private static int SetReturn(CpuContext ctx, int result)
{ {
ctx[CpuRegister.Rax] = unchecked((ulong)result); ctx[CpuRegister.Rax] = unchecked((ulong)result);
@@ -0,0 +1,76 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Np;
public static class NpEntitlementAccessExports
{
private const int BootParamClearSize = 0x20;
private const int EmptyAddcontInfoListSize = 0x10;
[SysAbiExport(
Nid = "jO8DM8oyego",
ExportName = "sceNpEntitlementAccessInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpEntitlementAccess")]
public static int NpEntitlementAccessInitialize(CpuContext ctx)
{
var initParam = ctx[CpuRegister.Rdi];
var bootParam = ctx[CpuRegister.Rsi];
if (bootParam != 0)
{
Span<byte> clear = stackalloc byte[BootParamClearSize];
clear.Clear();
if (!ctx.Memory.TryWrite(bootParam, clear))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "TFyU+KFBv54",
ExportName = "sceNpEntitlementAccessGetAddcontEntitlementInfoList",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceNpEntitlementAccess")]
public static int NpEntitlementAccessGetAddcontEntitlementInfoList(CpuContext ctx)
{
var listAddress = ctx[CpuRegister.Rsi];
if (listAddress != 0)
{
Span<byte> emptyList = stackalloc byte[EmptyAddcontInfoListSize];
emptyList.Clear();
if (!ctx.Memory.TryWrite(listAddress, emptyList))
{
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
TraceNpEntitlementAccess(
$"get_addcont_info_list service=0x{ctx[CpuRegister.Rdi]:X16} list=0x{listAddress:X16} " +
$"max={ctx[CpuRegister.Rdx]} flags=0x{ctx[CpuRegister.Rcx]:X16} -> empty");
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
private static void TraceNpEntitlementAccess(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] np.entitlement.{message}");
}
}
+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 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]; Span<byte> data = stackalloc byte[PadDataSize];
data.Clear(); data.Clear();
data[0x04] = 128; data[0x04] = 128;
@@ -145,9 +177,7 @@ public static class PadExports
timestampMicroseconds); timestampMicroseconds);
data[0x68] = 1; data[0x68] = 1;
return ctx.Memory.TryWrite(dataAddress, data) return ctx.Memory.TryWrite(dataAddress, data);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
private static int SetReturn(CpuContext ctx, int result) private static int SetReturn(CpuContext ctx, int result)
+57 -8
View File
@@ -29,6 +29,7 @@ public static class PlayGoExports
private const int PlayGoInstallSpeedTrickle = 1; private const int PlayGoInstallSpeedTrickle = 1;
private const int PlayGoInstallSpeedFull = 2; private const int PlayGoInstallSpeedFull = 2;
private const uint MaxPlayGoQueryEntries = 0x4000; private const uint MaxPlayGoQueryEntries = 0x4000;
private const uint PlayGoAllEntriesSentinel = uint.MaxValue;
private static readonly Regex ChunkIdPattern = new( private static readonly Regex ChunkIdPattern = new(
@"<chunk\s+[^>]*\bid\s*=\s*""(?<id>\d+)""", @"<chunk\s+[^>]*\bid\s*=\s*""(?<id>\d+)""",
@@ -44,6 +45,8 @@ public static class PlayGoExports
private static PlayGoMetadata _metadata = PlayGoMetadata.Empty; private static PlayGoMetadata _metadata = PlayGoMetadata.Empty;
private static int _installSpeed = PlayGoInstallSpeedTrickle; private static int _installSpeed = PlayGoInstallSpeedTrickle;
private static ulong _languageMask = ulong.MaxValue; private static ulong _languageMask = ulong.MaxValue;
private static int _unknownChunkDiagnostics;
private static int _locusTraceDiagnostics;
[SysAbiExport( [SysAbiExport(
Nid = "ts6GlZOKRrE", Nid = "ts6GlZOKRrE",
@@ -93,6 +96,7 @@ public static class PlayGoExports
_languageMask = ulong.MaxValue; _languageMask = ulong.MaxValue;
_opened = false; _opened = false;
_initialized = true; _initialized = true;
TracePlayGo($"initialize chunks={_metadata.ChunkIds.Length} available={_metadata.Available}");
} }
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -130,6 +134,7 @@ public static class PlayGoExports
} }
_opened = true; _opened = true;
TracePlayGo($"open handle={PlayGoHandle} chunks={_metadata.ChunkIds.Length}");
} }
Span<byte> handleBytes = stackalloc byte[sizeof(uint)]; Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
@@ -228,6 +233,7 @@ public static class PlayGoExports
var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length; var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length;
if (outChunkIdList == 0) if (outChunkIdList == 0)
{ {
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
return TryWriteUInt32(ctx, outEntries, availableEntries) return TryWriteUInt32(ctx, outEntries, availableEntries)
? (int)OrbisGen2Result.ORBIS_GEN2_OK ? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -236,6 +242,7 @@ public static class PlayGoExports
var entriesToWrite = Math.Min(numberOfEntries, availableEntries); var entriesToWrite = Math.Min(numberOfEntries, availableEntries);
if (entriesToWrite > MaxPlayGoQueryEntries) if (entriesToWrite > MaxPlayGoQueryEntries)
{ {
TracePlayGo($"get_chunk_id bad_size requested={numberOfEntries} available={availableEntries}");
return OrbisPlayGoErrorBadSize; return OrbisPlayGoErrorBadSize;
} }
@@ -248,6 +255,7 @@ public static class PlayGoExports
} }
} }
TracePlayGo($"get_chunk_id write requested={numberOfEntries} wrote={entriesToWrite} available={availableEntries}");
return TryWriteUInt32(ctx, outEntries, entriesToWrite) return TryWriteUInt32(ctx, outEntries, entriesToWrite)
? (int)OrbisGen2Result.ORBIS_GEN2_OK ? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -278,6 +286,7 @@ public static class PlayGoExports
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries) if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{ {
TracePlayGo($"get_eta bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outEta:X16}");
return OrbisPlayGoErrorBadSize; return OrbisPlayGoErrorBadSize;
} }
@@ -375,8 +384,15 @@ public static class PlayGoExports
return OrbisPlayGoErrorBadPointer; return OrbisPlayGoErrorBadPointer;
} }
if (numberOfEntries == PlayGoAllEntriesSentinel)
{
TracePlayGo($"get_locus sentinel entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries) if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{ {
TracePlayGo($"get_locus bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
return OrbisPlayGoErrorBadSize; return OrbisPlayGoErrorBadSize;
} }
@@ -390,15 +406,24 @@ public static class PlayGoExports
if (!IsKnownChunkId(chunkId)) if (!IsKnownChunkId(chunkId))
{ {
loci[i] = PlayGoLocusNotDownloaded; if (Interlocked.Increment(ref _unknownChunkDiagnostics) <= 8)
return ctx.Memory.TryWrite(outLoci, loci) {
? OrbisPlayGoErrorBadChunkId ushort[] knownChunkIds;
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; 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; loci[i] = PlayGoLocusLocalFast;
} }
TracePlayGoLocus(numberOfEntries, chunkIds, outLoci);
return ctx.Memory.TryWrite(outLoci, loci) return ctx.Memory.TryWrite(outLoci, loci)
? (int)OrbisGen2Result.ORBIS_GEN2_OK ? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -429,6 +454,7 @@ public static class PlayGoExports
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries) if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
{ {
TracePlayGo($"get_progress bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outProgress:X16}");
return OrbisPlayGoErrorBadSize; return OrbisPlayGoErrorBadSize;
} }
@@ -438,6 +464,7 @@ public static class PlayGoExports
return chunkError; return chunkError;
} }
TracePlayGo($"get_progress entries={numberOfEntries}");
Span<byte> progress = stackalloc byte[sizeof(ulong) * 2]; Span<byte> progress = stackalloc byte[sizeof(ulong) * 2];
BinaryPrimitives.WriteUInt64LittleEndian(progress, 0); BinaryPrimitives.WriteUInt64LittleEndian(progress, 0);
BinaryPrimitives.WriteUInt64LittleEndian(progress[sizeof(ulong)..], 0); BinaryPrimitives.WriteUInt64LittleEndian(progress[sizeof(ulong)..], 0);
@@ -471,9 +498,11 @@ public static class PlayGoExports
if (numberOfEntries == 0) if (numberOfEntries == 0)
{ {
TracePlayGo("get_todo bad_size entries=0");
return OrbisPlayGoErrorBadSize; return OrbisPlayGoErrorBadSize;
} }
TracePlayGo($"get_todo requested={numberOfEntries} wrote=0");
return TryWriteUInt32(ctx, outEntries, 0) return TryWriteUInt32(ctx, outEntries, 0)
? (int)OrbisGen2Result.ORBIS_GEN2_OK ? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; : (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
@@ -624,10 +653,7 @@ public static class PlayGoExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
if (!IsKnownChunkId(chunkId)) _ = chunkId;
{
return OrbisPlayGoErrorBadChunkId;
}
} }
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -743,6 +769,29 @@ public static class PlayGoExports
return ctx.Memory.TryWrite(address, buffer); return ctx.Memory.TryWrite(address, buffer);
} }
private static void TracePlayGo(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] playgo.{message}");
}
}
private static void TracePlayGoLocus(uint entries, ulong chunkIds, ulong outLoci)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
{
return;
}
var count = Interlocked.Increment(ref _locusTraceDiagnostics);
if (entries != 1 || count <= 32 || count % 1000 == 0)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] playgo.get_locus entries={entries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
}
}
private sealed record PlayGoMetadata(bool Available, ushort[] ChunkIds) private sealed record PlayGoMetadata(bool Available, ushort[] ChunkIds)
{ {
public static readonly PlayGoMetadata Empty = new(false, Array.Empty<ushort>()); 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 public static class RtcExports
{ {
private const long DateTimeTicksPerMicrosecond = 10;
[SysAbiExport( [SysAbiExport(
Nid = "ZPD1YOKI+Kw", Nid = "ZPD1YOKI+Kw",
ExportName = "sceRtcGetCurrentClockLocalTime", ExportName = "sceRtcGetCurrentClockLocalTime",
@@ -42,6 +44,29 @@ public static class RtcExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
Nid = "8w-H19ip48I", Nid = "8w-H19ip48I",
ExportName = "sceRtcGetTick", ExportName = "sceRtcGetTick",
@@ -72,14 +97,57 @@ public static class RtcExports
rtcDateTime.Minute, rtcDateTime.Minute,
rtcDateTime.Second, rtcDateTime.Second,
DateTimeKind.Utc); 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) catch (ArgumentOutOfRangeException)
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT; 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; return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
} }
@@ -108,6 +176,21 @@ public static class RtcExports
return true; 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( private readonly record struct RtcDateTime(
ushort Year, ushort Year,
ushort Month, ushort Month,
@@ -0,0 +1,213 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.SaveData;
public static class SaveDataDialogExports
{
private const int StatusNone = 0;
private const int StatusInitialized = 1;
private const int StatusRunning = 2;
private const int StatusFinished = 3;
private const int ErrorOk = 0;
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
private const int ErrorAlreadyInitialized = unchecked((int)0x80B80004);
private const int ErrorNotFinished = unchecked((int)0x80B80005);
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
private const int ErrorArgNull = unchecked((int)0x80B8000D);
private const int ResultSize = 0x48;
private static int _status;
private static int _lastMode;
private static ulong _lastUserData;
[SysAbiExport(
Nid = "s9e3+YpRnzw",
ExportName = "sceSaveDataDialogInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogInitialize(CpuContext ctx)
{
if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
{
return SetReturn(ctx, ErrorAlreadyInitialized);
}
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
Nid = "4tPhsP6FpDI",
ExportName = "sceSaveDataDialogOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogOpen(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return SetReturn(ctx, ErrorArgNull);
}
if (_status is not (StatusInitialized or StatusFinished))
{
return SetReturn(ctx, ErrorNotInitialized);
}
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
_lastUserData = TryReadUInt64(ctx, paramAddress + 0xC8, out var userData) ? userData : 0;
// There is no host save dialog yet. Complete immediately with OK so
// guest polling sees a finished dialog instead of spinning forever.
Interlocked.Exchange(ref _status, StatusFinished);
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished");
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
Nid = "ERKzksauAJA",
ExportName = "sceSaveDataDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "KK3Bdg1RWK0",
ExportName = "sceSaveDataDialogUpdateStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "en7gNVnh878",
ExportName = "sceSaveDataDialogIsReadyToDisplay",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => SetReturn(ctx, 1);
[SysAbiExport(
Nid = "yEiJ-qqr6Cg",
ExportName = "sceSaveDataDialogGetResult",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogGetResult(CpuContext ctx)
{
var resultAddress = ctx[CpuRegister.Rdi];
if (resultAddress == 0)
{
return SetReturn(ctx, ErrorArgNull);
}
if (Volatile.Read(ref _status) != StatusFinished)
{
return SetReturn(ctx, ErrorNotFinished);
}
Span<byte> result = stackalloc byte[ResultSize];
result.Clear();
BinaryPrimitives.WriteInt32LittleEndian(result[0x00..], _lastMode);
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], 0);
BinaryPrimitives.WriteUInt64LittleEndian(result[0x20..], _lastUserData);
if (!ctx.Memory.TryWrite(resultAddress, result))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
Nid = "fH46Lag88XY",
ExportName = "sceSaveDataDialogClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogClose(CpuContext ctx)
{
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
{
return SetReturn(ctx, ErrorNotRunning);
}
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
Nid = "YuH2FA7azqQ",
ExportName = "sceSaveDataDialogTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogTerminate(CpuContext ctx)
{
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
{
return SetReturn(ctx, ErrorNotInitialized);
}
_lastMode = 0;
_lastUserData = 0;
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
Nid = "V-uEeFKARJU",
ExportName = "sceSaveDataDialogProgressBarInc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => SetReturn(ctx, ErrorOk);
[SysAbiExport(
Nid = "hay1CfTmLyA",
ExportName = "sceSaveDataDialogProgressBarSetValue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => SetReturn(ctx, ErrorOk);
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
value = 0;
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(address, bytes))
{
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
{
value = 0;
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
if (!ctx.Memory.TryRead(address, bytes))
{
return false;
}
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
return true;
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceSaveDataDialog(string message)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
{
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] save_data_dialog.{message}");
}
}
@@ -0,0 +1,444 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
namespace SharpEmu.Libs.SaveData;
public static class SaveDataExports
{
private const int OrbisSaveDataErrorParameter = unchecked((int)0x809F0000);
private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B);
private const int SaveDataTitleIdSize = 10;
private const int SaveDataDirNameSize = 32;
private const int SaveDataParamSize = 0x530;
private const int SaveDataSearchInfoSize = 0x30;
private const ulong ResultHitNumOffset = 0x00;
private const ulong ResultDirNamesOffset = 0x08;
private const ulong ResultDirNamesNumOffset = 0x10;
private const ulong ResultSetNumOffset = 0x14;
private const ulong ResultParamsOffset = 0x18;
private const ulong ResultInfosOffset = 0x20;
private const uint SortKeyFreeBlocks = 5;
private const uint SortOrderDescent = 1;
private static readonly object _stateGate = new();
private static string? _titleId;
public static void ConfigureApplicationInfo(string? titleId)
{
lock (_stateGate)
{
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
}
}
[SysAbiExport(
Nid = "TywrFKCoLGY",
ExportName = "sceSaveDataInitialize3",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataInitialize3(CpuContext ctx)
{
try
{
Directory.CreateDirectory(ResolveSaveDataRoot());
return SetReturn(ctx, 0);
}
catch (IOException)
{
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (UnauthorizedAccessException)
{
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
}
[SysAbiExport(
Nid = "dyIhnXq-0SM",
ExportName = "sceSaveDataDirNameSearch",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataDirNameSearch(CpuContext ctx)
{
var condAddress = ctx[CpuRegister.Rdi];
var resultAddress = ctx[CpuRegister.Rsi];
if (condAddress == 0 || resultAddress == 0)
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
!TryReadSearchResult(ctx, resultAddress, out var result))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
try
{
string titleId;
if (cond.TitleIdAddress == 0)
{
titleId = ResolveConfiguredTitleId();
}
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
var entries = Directory.Exists(root)
? EnumerateSaveDirectories(root, cond.Pattern)
: [];
entries = SortEntries(entries, cond.SortKey, cond.SortOrder);
var setNum = result.DirNamesNum == 0
? 0
: Math.Min(result.DirNamesNum, entries.Count);
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (setNum == 0)
{
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
return SetReturn(ctx, 0);
}
if (result.DirNamesAddress == 0)
{
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
for (var i = 0; i < setNum; i++)
{
var entry = entries[i];
if (!TryWriteFixedAscii(
ctx,
result.DirNamesAddress + ((ulong)i * SaveDataDirNameSize),
SaveDataDirNameSize,
entry.Name) ||
(result.ParamsAddress != 0 &&
!TryWriteParam(ctx, result.ParamsAddress + ((ulong)i * SaveDataParamSize), entry)) ||
(result.InfosAddress != 0 &&
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
{
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
return SetReturn(ctx, 0);
}
catch (IOException)
{
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (UnauthorizedAccessException)
{
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
}
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
{
cond = default;
if (!TryReadInt32(ctx, address, out var userId) ||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
{
return false;
}
string pattern;
if (dirNameAddress == 0)
{
pattern = string.Empty;
}
else if (!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out pattern))
{
return false;
}
cond = new SearchCond(userId, titleIdAddress, pattern, sortKey, sortOrder);
return true;
}
private static bool TryReadSearchResult(CpuContext ctx, ulong address, out SearchResult result)
{
result = default;
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
!TryReadUInt32(ctx, address + ResultDirNamesNumOffset, out var dirNamesNum) ||
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
{
return false;
}
result = new SearchResult(dirNamesAddress, dirNamesNum, paramsAddress, infosAddress);
return true;
}
private static List<SaveEntry> EnumerateSaveDirectories(string root, string pattern)
{
var entries = new List<SaveEntry>();
foreach (var directory in Directory.EnumerateDirectories(root))
{
var name = Path.GetFileName(directory);
if (string.IsNullOrWhiteSpace(name) ||
name.StartsWith("sce_", StringComparison.OrdinalIgnoreCase) ||
(!string.IsNullOrEmpty(pattern) && !MatchPattern(name, pattern)))
{
continue;
}
var info = new DirectoryInfo(directory);
entries.Add(new SaveEntry(name, directory, info.LastWriteTimeUtc));
}
return entries;
}
private static List<SaveEntry> SortEntries(List<SaveEntry> entries, uint sortKey, uint sortOrder)
{
IOrderedEnumerable<SaveEntry> sorted = sortKey switch
{
3 => entries.OrderBy(entry => entry.LastWriteUtc),
_ => entries.OrderBy(entry => entry.Name, StringComparer.OrdinalIgnoreCase),
};
var list = sorted.ToList();
if (sortOrder == SortOrderDescent)
{
list.Reverse();
}
return list;
}
private static bool TryWriteParam(CpuContext ctx, ulong address, SaveEntry entry)
{
var param = new byte[SaveDataParamSize];
WriteAscii(param.AsSpan(0x00, 128), "Saved Data");
WriteAscii(param.AsSpan(0x100, 1024), entry.Name);
BinaryPrimitives.WriteInt64LittleEndian(
param.AsSpan(0x508, sizeof(long)),
new DateTimeOffset(entry.LastWriteUtc).ToUnixTimeSeconds());
return ctx.Memory.TryWrite(address, param);
}
private static bool TryWriteSearchInfo(CpuContext ctx, ulong address, SaveEntry entry)
{
var size = GetDirectorySize(entry.Path);
var usedBlocks = checked((ulong)((size + 32767) / 32768));
var blocks = Math.Max(96UL, usedBlocks);
Span<byte> info = stackalloc byte[SaveDataSearchInfoSize];
info.Clear();
BinaryPrimitives.WriteUInt64LittleEndian(info[0x00..], blocks);
BinaryPrimitives.WriteUInt64LittleEndian(info[0x08..], blocks - usedBlocks);
return ctx.Memory.TryWrite(address, info);
}
private static long GetDirectorySize(string root)
{
long total = 0;
foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories))
{
total += new FileInfo(file).Length;
}
return total;
}
private static bool MatchPattern(string value, string pattern) =>
MatchPattern(value.AsSpan(), pattern.AsSpan());
private static bool MatchPattern(ReadOnlySpan<char> value, ReadOnlySpan<char> pattern)
{
if (pattern.IsEmpty)
{
return value.IsEmpty;
}
if (pattern[0] == '%')
{
for (var i = 0; i <= value.Length; i++)
{
if (MatchPattern(value[i..], pattern[1..]))
{
return true;
}
}
return false;
}
if (value.IsEmpty)
{
return false;
}
if (pattern[0] == '_' ||
char.ToUpperInvariant(pattern[0]) == char.ToUpperInvariant(value[0]))
{
return MatchPattern(value[1..], pattern[1..]);
}
return false;
}
private static string ResolveTitleSaveRoot(int userId, string titleId) =>
Path.Combine(ResolveSaveDataRoot(), userId.ToString(), SanitizePathSegment(titleId));
private static string ResolveSaveDataRoot()
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
var root = string.IsNullOrWhiteSpace(configured)
? Path.Combine(Environment.CurrentDirectory, "user", "savedata")
: configured;
return Path.GetFullPath(root);
}
private static string ResolveConfiguredTitleId()
{
lock (_stateGate)
{
if (!string.IsNullOrWhiteSpace(_titleId))
{
return _titleId;
}
}
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
var app0Name = string.IsNullOrWhiteSpace(app0Root)
? null
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
if (!string.IsNullOrWhiteSpace(app0Name))
{
var candidate = app0Name.Split('-', StringSplitOptions.RemoveEmptyEntries)[0];
if (!string.IsNullOrWhiteSpace(candidate))
{
return SanitizePathSegment(candidate);
}
}
return "default";
}
private static string SanitizePathSegment(string value)
{
var invalid = Path.GetInvalidFileNameChars();
var sanitized = new string(value.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray());
return string.IsNullOrWhiteSpace(sanitized) ? "default" : sanitized;
}
private static bool TryReadFixedAscii(CpuContext ctx, ulong address, int length, out string value)
{
value = string.Empty;
Span<byte> buffer = stackalloc byte[length];
if (!ctx.Memory.TryRead(address, buffer))
{
return false;
}
var stringLength = buffer.IndexOf((byte)0);
if (stringLength < 0)
{
stringLength = buffer.Length;
}
value = Encoding.ASCII.GetString(buffer[..stringLength]);
return true;
}
private static bool TryWriteFixedAscii(CpuContext ctx, ulong address, int length, string value)
{
Span<byte> buffer = stackalloc byte[length];
buffer.Clear();
WriteAscii(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static void WriteAscii(Span<byte> destination, string value)
{
var count = Math.Min(value.Length, Math.Max(0, destination.Length - 1));
for (var i = 0; i < count; i++)
{
var ch = value[i];
destination[i] = ch <= 0x7F ? (byte)ch : (byte)'?';
}
}
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
Span<byte> bytes = stackalloc byte[sizeof(int)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
return true;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, bytes))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceSaveData(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] savedata.{message}");
}
}
private readonly record struct SearchCond(
int UserId,
ulong TitleIdAddress,
string Pattern,
uint SortKey,
uint SortOrder);
private readonly record struct SearchResult(
ulong DirNamesAddress,
uint DirNamesNum,
ulong ParamsAddress,
ulong InfosAddress);
private readonly record struct SaveEntry(string Name, string Path, DateTime LastWriteUtc);
}
+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;
}
}
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Libs.VideoOut;
using System.Buffers.Binary; using System.Buffers.Binary;
namespace SharpEmu.Libs.SystemService; namespace SharpEmu.Libs.SystemService;
@@ -12,6 +13,34 @@ public static class SystemServiceExports
private const int SystemServiceStatusSize = 0x0C; private const int SystemServiceStatusSize = 0x0C;
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128; 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( [SysAbiExport(
Nid = "rPo6tV8D9bM", Nid = "rPo6tV8D9bM",
ExportName = "sceSystemServiceGetStatus", ExportName = "sceSystemServiceGetStatus",
@@ -57,6 +86,17 @@ public static class SystemServiceExports
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
[SysAbiExport(
Nid = "Vo5V8KAwCmk",
ExportName = "sceSystemServiceHideSplashScreen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSystemService")]
public static int SystemServiceHideSplashScreen(CpuContext ctx)
{
VulkanVideoPresenter.HideSplashScreen();
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result) private static int SetReturn(CpuContext ctx, int result)
{ {
ctx[CpuRegister.Rax] = unchecked((ulong)result); ctx[CpuRegister.Rax] = unchecked((ulong)result);
@@ -3,14 +3,21 @@
using SharpEmu.HLE; using SharpEmu.HLE;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text;
using System.Threading;
namespace SharpEmu.Libs.UserService; namespace SharpEmu.Libs.UserService;
public static class UserServiceExports public static class UserServiceExports
{ {
private const int OrbisUserServiceErrorInvalidArgument = unchecked((int)0x80960005); 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 PrimaryUserId = 1;
private const int InvalidUserId = -1; private const int InvalidUserId = -1;
private const string PrimaryUserName = "SharpEmu";
private static int _loginEventDelivered;
[SysAbiExport( [SysAbiExport(
Nid = "j3YMu1MVNNo", Nid = "j3YMu1MVNNo",
@@ -64,6 +71,89 @@ public static class UserServiceExports
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); : 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) private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
{ {
Span<byte> bytes = stackalloc byte[sizeof(int)]; 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;
}
}
+117 -1
View File
@@ -29,8 +29,11 @@ public static class VideoOutExports
private const int VideoOutBufferAttributeSize = 0x28; private const int VideoOutBufferAttributeSize = 0x28;
private const int VideoOutBufferAttribute2Size = 0x50; private const int VideoOutBufferAttribute2Size = 0x50;
private const int VideoOutBuffersEntrySize = 0x20; private const int VideoOutBuffersEntrySize = 0x20;
private const int VideoOutOutputStatusSize = 0x30;
private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000; private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200; private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
private const ulong SceVideoOutPixelFormatB8G8R8A8Unorm = 0x8100000000000000;
private const ulong SceVideoOutPixelFormatR8G8B8A8Unorm = 0x8100000022000000;
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000; private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000; private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000; private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
@@ -82,6 +85,10 @@ public static class VideoOutExports
public ulong VblankCount { get; set; } public ulong VblankCount { get; set; }
public ulong FlipCount { get; set; } public ulong FlipCount { get; set; }
public int CurrentBuffer { get; set; } = -1; 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 VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots(); public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
public List<FlipEventRegistration> FlipEvents { get; } = new(); public List<FlipEventRegistration> FlipEvents { get; } = new();
@@ -196,6 +203,93 @@ public static class VideoOutExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; 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( [SysAbiExport(
Nid = "j6RaAUlaLv0", Nid = "j6RaAUlaLv0",
ExportName = "sceVideoOutWaitVblank", ExportName = "sceVideoOutWaitVblank",
@@ -269,6 +363,23 @@ public static class VideoOutExports
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg); return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
} }
[SysAbiExport(
Nid = "zgXifHT9ErY",
ExportName = "sceVideoOutIsFlipPending",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutIsFlipPending(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!TryGetPort(handle, out _))
{
return OrbisVideoOutErrorInvalidHandle;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport( [SysAbiExport(
Nid = "U2JJtSqNKZI", Nid = "U2JJtSqNKZI",
ExportName = "sceVideoOutGetEventId", ExportName = "sceVideoOutGetEventId",
@@ -715,6 +826,8 @@ public static class VideoOutExports
Index = groupIndex, Index = groupIndex,
Attribute = attribute, Attribute = attribute,
}; };
port.OutputWidth = attribute.Width;
port.OutputHeight = attribute.Height;
for (var i = 0; i < addresses.Length; i++) for (var i = 0; i < addresses.Length; i++)
{ {
@@ -726,6 +839,7 @@ public static class VideoOutExports
TraceVideoOut( 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}"); $"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; return groupIndex;
} }
} }
@@ -939,6 +1053,8 @@ public static class VideoOutExports
private static uint GetBytesPerPixel(ulong pixelFormat) => private static uint GetBytesPerPixel(ulong pixelFormat) =>
pixelFormat is SceVideoOutPixelFormatA8R8G8B8Srgb or pixelFormat is SceVideoOutPixelFormatA8R8G8B8Srgb or
SceVideoOutPixelFormatA8B8G8R8Srgb or SceVideoOutPixelFormatA8B8G8R8Srgb or
SceVideoOutPixelFormatB8G8R8A8Unorm or
SceVideoOutPixelFormatR8G8B8A8Unorm or
SceVideoOutPixelFormatA2R10G10B10 or SceVideoOutPixelFormatA2R10G10B10 or
SceVideoOutPixelFormatA2R10G10B10Srgb or SceVideoOutPixelFormatA2R10G10B10Srgb or
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq SceVideoOutPixelFormatA2R10G10B10Bt2020Pq
@@ -973,7 +1089,7 @@ public static class VideoOutExports
var dst = 0; var dst = 0;
for (var src = 0; src + 3 < source.Length; src += 4) 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 + 0];
destination[dst++] = source[src + 1]; destination[dst++] = source[src + 1];
@@ -23,7 +23,92 @@ internal static unsafe class VulkanVideoPresenter
private static readonly object _gate = new(); private static readonly object _gate = new();
private static Thread? _thread; private static Thread? _thread;
private static Presentation? _latestPresentation; private static Presentation? _latestPresentation;
private static uint _windowWidth;
private static uint _windowHeight;
private static bool _closed; private static bool _closed;
private static bool _splashHidden;
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 ??= _splashHidden
? new Presentation(
CreateBlackFrame(width, height),
width,
height,
1,
GuestDrawKind.None,
IsSplash: false)
: 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 HideSplashScreen()
{
lock (_gate)
{
_splashHidden = true;
if (_closed || _latestPresentation is not { IsSplash: true } latest)
{
return;
}
var sequence = latest.Sequence + 1;
_latestPresentation = new Presentation(
CreateBlackFrame(latest.Width, latest.Height),
latest.Width,
latest.Height,
sequence,
GuestDrawKind.None,
IsSplash: false);
Console.Error.WriteLine("[LOADER][INFO] Vulkan VideoOut hid splash");
}
}
public static void Submit(byte[] bgraFrame, uint width, uint height) public static void Submit(byte[] bgraFrame, uint width, uint height)
{ {
@@ -40,12 +125,20 @@ internal static unsafe class VulkanVideoPresenter
} }
var sequence = (_latestPresentation?.Sequence ?? 0) + 1; 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) if (_thread is not null)
{ {
return; return;
} }
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run) _thread = new Thread(Run)
{ {
IsBackground = true, IsBackground = true,
@@ -74,12 +167,20 @@ internal static unsafe class VulkanVideoPresenter
} }
var sequence = (_latestPresentation?.Sequence ?? 0) + 1; 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) if (_thread is not null)
{ {
return; return;
} }
_windowWidth = width;
_windowHeight = height;
_thread = new Thread(Run) _thread = new Thread(Run)
{ {
IsBackground = true, IsBackground = true,
@@ -89,14 +190,32 @@ internal static unsafe class VulkanVideoPresenter
} }
} }
private static byte[] CreateBlackFrame(uint width, uint height)
{
if (width == 0 || height == 0 || width > 8192 || height > 8192)
{
width = 1;
height = 1;
}
var pixels = GC.AllocateUninitializedArray<byte>(checked((int)(width * height * 4)));
pixels.AsSpan().Clear();
for (var offset = 3; offset < pixels.Length; offset += 4)
{
pixels[offset] = 0xFF;
}
return pixels;
}
private static void Run() private static void Run()
{ {
uint width; uint width;
uint height; uint height;
lock (_gate) lock (_gate)
{ {
width = _latestPresentation?.Width ?? 1280; width = _windowWidth == 0 ? _latestPresentation?.Width ?? 1280 : _windowWidth;
height = _latestPresentation?.Height ?? 720; height = _windowHeight == 0 ? _latestPresentation?.Height ?? 720 : _windowHeight;
} }
try try
@@ -138,7 +257,8 @@ internal static unsafe class VulkanVideoPresenter
uint Width, uint Width,
uint Height, uint Height,
long Sequence, long Sequence,
GuestDrawKind DrawKind); GuestDrawKind DrawKind,
bool IsSplash);
private sealed class Presenter : IDisposable private sealed class Presenter : IDisposable
{ {
@@ -179,6 +299,7 @@ internal static unsafe class VulkanVideoPresenter
private bool _vulkanReady; private bool _vulkanReady;
private bool _firstFramePresented; private bool _firstFramePresented;
private bool _firstGuestDrawPresented; private bool _firstGuestDrawPresented;
private bool _splashPresented;
public Presenter(uint width, uint height) public Presenter(uint width, uint height)
{ {
@@ -187,6 +308,8 @@ internal static unsafe class VulkanVideoPresenter
options.Title = VideoOutExports.GetWindowTitle(); options.Title = VideoOutExports.GetWindowTitle();
options.WindowBorder = WindowBorder.Fixed; options.WindowBorder = WindowBorder.Fixed;
options.VSync = true; options.VSync = true;
options.FramesPerSecond = 60;
options.UpdatesPerSecond = 60;
_window = Window.Create(options); _window = Window.Create(options);
_window.Load += Initialize; _window.Load += Initialize;
_window.Render += Render; _window.Render += Render;
@@ -813,7 +936,14 @@ internal static unsafe class VulkanVideoPresenter
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle"); Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
_imageInitialized[imageIndex] = true; _imageInitialized[imageIndex] = true;
_presentedSequence = presentation.Sequence; _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; _firstFramePresented = true;
Console.Error.WriteLine( Console.Error.WriteLine(