Compare commits

...

18 Commits

Author SHA1 Message Date
ParantezTech bbf5ff7be8 more HLE's and fix cpu execution some titles 2026-05-10 19:51:57 +03:00
ParantezTech 0c859f04ad Added more HLE's 2026-04-27 12:40:50 +03:00
ParantezTech 994438963d [ci] temporary fix 2026-04-13 15:59:19 +03:00
ParantezTech 233af123af More new HLE's and fixes 2026-04-13 15:54:38 +03:00
ParantezTech 02eb9b30e9 correct patch 2026-03-28 18:40:54 +03:00
ParantezTech fa46819030 fix for actions (again) 2026-03-28 18:23:46 +03:00
ParantezTech 7a915e88dc fix dotnet version on Github Actions 2026-03-28 18:15:14 +03:00
ParantezTech 812879aa81 PlayGo, VideoOut minimum HLE implements and fix some direct runner 2026-03-28 18:02:37 +03:00
ParantezTech 71ba5cf1db More HLE implements: wcs* 2026-03-19 23:57:48 +03:00
ParantezTech 8f108ca01d Upgrade GitHub Actions to Node 24 2026-03-19 13:33:19 +03:00
ParantezTech cecf55bf2e fix: dereference of a possibly null reference. 2026-03-16 21:19:49 +03:00
ParantezTech 0ede7c70a9 [ci] update actions Node 20 deprecation 2026-03-16 21:18:58 +03:00
ParantezTech 8c39a55501 reuse 2026-03-16 20:49:40 +03:00
ParantezTech 0a4b36ffcb [dotnet/ci] constant version 2026-03-16 20:46:47 +03:00
ParantezTech 1a1d61b21c rework TLS allocations, more HLE's, increase import loop history 2026-03-16 20:22:58 +03:00
ParantezTech 3cf96f38de [github] bug_report fix 2026-03-16 20:14:58 +03:00
ParantezTech d8c44bb64f [github] added bug report issue template 2026-03-16 20:11:56 +03:00
ParantezTech a4b5171586 [github] added issue config & reuse 2026-03-16 20:01:08 +03:00
26 changed files with 5387 additions and 267 deletions
+59
View File
@@ -0,0 +1,59 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
name: Bug Report
description: Report a bug in the emulator
labels: ["bug"]
body:
- type: markdown
attributes:
value: |
### Before creating an issue
- Do not create issues involving **software piracy**. Our rules specifically prohibit this.
Issues related to piracy will be closed and the author may be banned from the repository.
- The issue tracker is **not for asking questions**.
Question-type issues will be closed.
- Only open an issue if you are reporting an actual **bug in the emulator**.
- type: input
id: emulator_version
attributes:
label: Emulator Version / Commit
description: The emulator version or git commit where the bug occurred
placeholder: e.g. v0.0.2 / a1b2c3d
validations:
required: true
- type: textarea
id: description
attributes:
label: Bug Description
description: Describe the problem and how to reproduce it
placeholder: |
Steps to reproduce:
1. Launch emulator
2. Load game
3. Crash occurs
Expected behavior:
...
Actual behavior:
...
validations:
required: true
- type: textarea
id: logs
attributes:
label: Log File
description: |
Drag & drop your log file here, or paste the log contents.
Supported: .log / .txt / .zip
Large logs should be uploaded as a file.
render: shell
+4
View File
@@ -0,0 +1,4 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
blank_issues_enabled: false
@@ -1,3 +1,6 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
name: Game Compatibility Report
description: Report compatibility status of a game
labels: ["game-compatibility"]
+6 -3
View File
@@ -1,3 +1,6 @@
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
name: Build and Release
on:
@@ -79,7 +82,7 @@ jobs:
- name: Setup .NET SDK
uses: actions/setup-dotnet@v5
with:
dotnet-version: 10.0.x
dotnet-version: 10.0.103
cache: true
cache-dependency-path: |
Directory.Packages.props
@@ -106,7 +109,7 @@ jobs:
Compress-Archive -Path (Join-Path $env:PUBLISH_DIR '*') -DestinationPath $archivePath -CompressionLevel Optimal
- name: Upload build artifact
uses: actions/upload-artifact@v4
uses: actions/upload-artifact@v7
with:
name: ${{ needs.init.outputs.artifact-name }}
path: ${{ env.RELEASE_DIR }}\${{ needs.init.outputs.archive-name }}
@@ -123,7 +126,7 @@ jobs:
contents: write
steps:
- name: Download build artifact
uses: actions/download-artifact@v4
uses: actions/download-artifact@v8
with:
name: ${{ needs.init.outputs.artifact-name }}
path: release
+2
View File
@@ -29,6 +29,8 @@ arm64/
packages/
*.nupkg
.nuget/
.dotnet-home/
.cache/
.DS_Store
Thumbs.db
+1
View File
@@ -3,6 +3,7 @@ version = 1
[[annotations]]
path = [
"REUSE.toml",
"global.json",
"**/packages.lock.json",
"scripts/ps5_names.txt",
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
+6
View File
@@ -0,0 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "disable"
}
}
+22
View File
@@ -193,6 +193,11 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!InitializeGuestFrameChainSentinel(context))
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!InitializeTls(context, tlsBase))
{
return FailEarly(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
@@ -363,6 +368,23 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL);
}
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
{
var stackTop = context[CpuRegister.Rsp] + sizeof(ulong);
var sentinelFrame = AlignDown(stackTop - 0x20, 16);
var seedRsp = sentinelFrame - sizeof(ulong);
if (!context.TryWriteUInt64(sentinelFrame, 0) ||
!context.TryWriteUInt64(sentinelFrame + sizeof(ulong), 0) ||
!context.TryWriteUInt64(seedRsp, 0))
{
return false;
}
context[CpuRegister.Rbp] = sentinelFrame;
context[CpuRegister.Rsp] = seedRsp;
return true;
}
private static bool InitializeProcessEntryFrame(
CpuContext context,
string processImageName,
@@ -154,15 +154,57 @@ public sealed partial class DirectExecutionBackend
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
bool flag3 = num >= 1020 && num <= 1040;
if (!flag0 && (num <= 128 || (num >= 240 && num <= 400) || (num >= 900 && num <= 1300) || num % 100000 == 0L || (importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) || (importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) || flag || flag2 || flag3))
bool logAllImports = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_ALL_IMPORTS"), "1", StringComparison.Ordinal);
string importFilter = Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FILTER");
bool flag4 = !string.IsNullOrWhiteSpace(importFilter);
bool flag5 = false;
ExportedFunction matchedExport = null;
if (_moduleManager.TryGetExport(importStubEntry.Nid, out ExportedFunction export))
{
if (_moduleManager.TryGetExport(importStubEntry.Nid, out ExportedFunction export))
matchedExport = export;
if (flag4)
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid})");
flag5 = export.LibraryName.Contains(importFilter, StringComparison.OrdinalIgnoreCase)
|| export.Name.Contains(importFilter, StringComparison.OrdinalIgnoreCase)
|| importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase);
}
}
else if (flag4)
{
flag5 = importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase);
}
bool flag6 = logAllImports || flag5;
if (!flag0 && (flag6 || num <= 128 || (num >= 240 && num <= 400) || (num >= 900 && num <= 1300) || num % 100000 == 0L || (importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) || (importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) || flag || flag2 || flag3))
{
if (matchedExport != null)
{
if (flag6)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{num}: {matchedExport.LibraryName}:{matchedExport.Name} ({importStubEntry.Nid}) " +
$"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} ret=0x{num7:X16}");
}
else
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {matchedExport.LibraryName}:{matchedExport.Name} ({importStubEntry.Nid})");
}
}
else
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {importStubEntry.Nid}");
if (flag6)
{
Console.Error.WriteLine(
$"[LOADER][TRACE] Import#{num}: {importStubEntry.Nid} " +
$"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} ret=0x{num7:X16}");
}
else
{
Console.Error.WriteLine($"[LOADER][TRACE] Import#{num}: {importStubEntry.Nid}");
}
}
if (flag6)
{
Console.Error.Flush();
}
}
if (!flag0)
@@ -212,28 +254,6 @@ public sealed partial class DirectExecutionBackend
}
TryBypassStackChkFailTrap(num, num7);
}
if (importStubEntry.Nid == "9rAeANT2tyE" || importStubEntry.Nid == "1j3S3n-tTW4")
{
ulong rspDbg = _cpuContext[CpuRegister.Rsp];
Console.Error.WriteLine(
$"[LOADER][TRACE] ImportDbg#{num}: nid={importStubEntry.Nid} " +
$"ret=0x{num7:X16} rsp=0x{rspDbg:X16} rsp&7=0x{(rspDbg & 7):X} rsp&F=0x{(rspDbg & 0xF):X}");
if (_cpuContext.TryReadUInt64(rspDbg, out var s0))
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x00] = 0x{s0:X16}");
if (_cpuContext.TryReadUInt64(rspDbg + 8, out var s8))
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x08] = 0x{s8:X16}");
if (_cpuContext.TryReadUInt64(rspDbg + 16, out var s10))
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x10] = 0x{s10:X16}");
if (_cpuContext.TryReadUInt64(rspDbg + 24, out var s18))
Console.Error.WriteLine($"[LOADER][TRACE] [rsp+0x18] = 0x{s18:X16}");
ProbeReturnRip(num7, num);
Console.Error.Flush();
}
ProbeReturnRip(0x0000000800EA020Eul, 999001);
ProbeReturnRip(0x0000000800EA0213ul, 999002);
ProbeReturnRip(0x0000000800EA040Aul, 999003);
try
{
OrbisGen2Result orbisGen2Result;
@@ -282,6 +302,23 @@ public sealed partial class DirectExecutionBackend
_cpuContext[CpuRegister.R15] = value8;
_cpuContext[CpuRegister.Rdi] = value;
_cpuContext[CpuRegister.Rsi] = value2;
if (GuestThreadExecution.TryConsumeCurrentEntryExit(out var exitStatus, out var exitReason))
{
if (TryCompleteGuestEntryToHostStub(argPackPtr, num, num7, importStubEntry.Nid, exitReason, exitStatus))
{
_cpuContext[CpuRegister.Rax] = unchecked((ulong)exitStatus);
}
else
{
LastError = $"Failed to complete guest entry after {importStubEntry.Nid}: missing host return sentinel";
_cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
}
}
if (GuestThreadExecution.TryConsumeCurrentThreadBlock(out var blockReason) &&
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
{
_cpuContext[CpuRegister.Rax] = 0uL;
}
if (flag || flag2 || flag3)
{
Console.Error.WriteLine($"[LOADER][TRACE] ImportRet#{num}: nid={importStubEntry.Nid} result={orbisGen2Result} rax=0x{_cpuContext[CpuRegister.Rax]:X16}");
@@ -322,6 +359,49 @@ public sealed partial class DirectExecutionBackend
return true;
}
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, int status)
{
ulong hostExit = _entryReturnSentinelRip;
if (hostExit < 65536)
{
return false;
}
try
{
*(ulong*)(argPackPtr + 96) = hostExit;
}
catch
{
return false;
}
Console.Error.WriteLine(
$"[LOADER][INFO] Guest entry exit at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={reason} status={status}");
return true;
}
private unsafe bool TryYieldGuestThreadToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason)
{
ulong hostExit = _entryReturnSentinelRip;
if (hostExit < 65536)
{
return false;
}
try
{
*(ulong*)(argPackPtr + 96) = hostExit;
}
catch
{
return false;
}
_guestThreadYieldRequested = true;
_guestThreadYieldReason = string.IsNullOrWhiteSpace(reason) ? nid : reason;
Console.Error.WriteLine(
$"[LOADER][INFO] Guest thread yield at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={_guestThreadYieldReason}");
return true;
}
private bool ShouldForceGuestExitOnImportLoop(string nid, ulong returnRip, long dispatchIndex, ulong arg0, ulong arg1)
{
if (dispatchIndex < 1200)
@@ -426,7 +506,20 @@ public sealed partial class DirectExecutionBackend
return false;
}
int num2 = CountDistinctImportLoopValuesFromTail(_importLoopReturnRips, sampleCount, 3);
return num2 <= 2;
if (num2 > 2)
{
return false;
}
int num3 = Math.Min(_importLoopSignatureCount, Math.Max(sampleCount * 8, ImportLoopWideDiversityWindow));
if (num3 <= sampleCount)
{
return true;
}
if (CountDistinctImportLoopValuesFromTail(_importLoopNidHashes, num3, 3) > 2)
{
return false;
}
return CountDistinctImportLoopValuesFromTail(_importLoopReturnRips, num3, 3) <= 2;
}
private int CountDistinctImportLoopValuesFromTail(ulong[] source, int sampleCount, int stopAfter)
@@ -7,12 +7,19 @@ using System.Diagnostics;
using System.Linq;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, IDisposable
public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, IGuestThreadScheduler, IDisposable
{
private const int ImportLoopHistoryLength = 2048;
private const int ImportLoopWideDiversityWindow = 768;
private readonly struct ImportStubEntry
{
public ulong Address { get; }
@@ -83,13 +90,23 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private const ulong GuestImageScanEnd = 36507222016uL;
private const ulong GuestThreadStackBaseAddress = 0x7FFF_E000_0000UL;
private const ulong GuestThreadTlsBaseAddress = 0x7FFE_0000_0000UL;
private const ulong GuestThreadStackSize = 0x0020_0000UL;
private const ulong GuestThreadTlsSize = 0x0001_0000UL;
private const ulong GuestThreadRegionStride = 0x0100_0000UL;
private const uint PAGE_EXECUTE_READWRITE = 64u;
private const uint PAGE_READWRITE = 4u;
private const uint PAGE_EXECUTE_READ = 32u;
private const int TlsHandlerRegionSize = 4096;
private const int TlsHandlerRegionSize = 16384;
private const ulong TlsModuleAllocStart = 140726751354880uL;
@@ -101,6 +118,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private nint _tlsBaseAddress;
private nint _ownedTlsBaseAddress;
private bool _ownsTlsBaseAddress;
private int _tlsPatchStubOffset;
@@ -179,11 +198,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private ulong _entryReturnSentinelRip;
private readonly ulong[] _importLoopSignatures = new ulong[192];
private readonly ulong[] _importLoopSignatures = new ulong[ImportLoopHistoryLength];
private readonly ulong[] _importLoopNidHashes = new ulong[192];
private readonly ulong[] _importLoopNidHashes = new ulong[ImportLoopHistoryLength];
private readonly ulong[] _importLoopReturnRips = new ulong[192];
private readonly ulong[] _importLoopReturnRips = new ulong[ImportLoopHistoryLength];
private int _importLoopSignatureCount;
@@ -193,6 +212,52 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private readonly Dictionary<string, ulong> _importNidHashCache = new Dictionary<string, ulong>(StringComparer.Ordinal);
private enum GuestThreadRunState
{
Ready,
Running,
Blocked,
Exited,
Faulted,
}
private enum GuestNativeCallExitReason
{
Returned,
Blocked,
ForcedExit,
Exception,
}
private sealed class GuestThreadState
{
public ulong ThreadHandle { get; init; }
public ulong EntryPoint { get; init; }
public ulong Argument { get; init; }
public string Name { get; init; } = string.Empty;
public CpuContext Context { get; init; } = null!;
public GuestThreadRunState State { get; set; }
public string? BlockReason { get; set; }
}
private readonly object _guestThreadGate = new object();
private readonly Queue<GuestThreadState> _readyGuestThreads = new Queue<GuestThreadState>();
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
private int _guestThreadPumpDepth;
private bool _guestThreadYieldRequested;
private string? _guestThreadYieldReason;
private bool _forcedGuestExit;
private ulong _lastAvTraceRip;
@@ -215,7 +280,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private nint _selfHandlePtr;
private static readonly byte[] TlsPattern = new byte[9] { 100, 72, 139, 4, 37, 0, 0, 0, 0 };
private const int MinTlsPatchInstructionBytes = 9;
private delegate ulong ImportGatewayDelegate(nint backendHandle, int importIndex, nint argPackPtr);
private delegate int RawExceptionHandlerDelegate(void* exceptionInfo);
@@ -332,6 +397,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
throw new OutOfMemoryException("Failed to allocate TLS base");
}
_ownedTlsBaseAddress = _tlsBaseAddress;
_ownsTlsBaseAddress = true;
SeedTlsLayout(_tlsBaseAddress);
_hostRspSlotStorage = (nint)VirtualAlloc(null, 4096u, 12288u, 4u);
@@ -372,12 +438,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_importLoopSignatureWriteIndex = 0;
_importLoopPatternHits = 0;
_importNidHashCache.Clear();
ClearGuestThreads();
_contextualUnresolvedReturnSites.Clear();
_stallWatchdogTriggered = 0;
_stallWatchdogStop = false;
_patchedEa020eLookupCall = false;
MarkExecutionProgress();
BindTlsBase(context);
var previousGuestThreadScheduler = GuestThreadExecution.Scheduler;
GuestThreadExecution.Scheduler = this;
try
{
if (!SetupImportStubs(importStubs))
@@ -403,6 +472,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
finally
{
GuestThreadExecution.Scheduler = previousGuestThreadScheduler;
Console.Error.WriteLine("[LOADER][INFO] === Execute END (LastError: " + (LastError ?? "null") + ") ===");
}
}
@@ -601,14 +671,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
return exportName switch
{
"_init_env" or
"atexit" or
"strlen" or
"strnlen" or
"strcmp" or
"strncmp" or
"strcpy" or
"strncpy" or
"memcpy" or
"memmove" or
"memset" or
@@ -649,18 +711,15 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
if (num != _tlsBaseAddress)
{
if (_ownsTlsBaseAddress && _tlsBaseAddress != 0)
{
VirtualFree((void*)_tlsBaseAddress, 0u, 32768u);
_ownsTlsBaseAddress = false;
}
_tlsBaseAddress = num;
_ownsTlsBaseAddress = _tlsBaseAddress == _ownedTlsBaseAddress;
}
if (_tlsBaseAddress != 0)
{
context.FsBase = (ulong)_tlsBaseAddress;
context.GsBase = (ulong)_tlsBaseAddress;
SeedTlsLayout(_tlsBaseAddress);
UpdateTlsHandlerBase(_tlsBaseAddress);
}
}
@@ -673,6 +732,30 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
*(ulong*)(tlsBase + 96) = num;
}
private unsafe void UpdateTlsHandlerBase(nint tlsBase)
{
if (_tlsHandlerAddress == 0)
{
return;
}
uint oldProtect = default;
if (!VirtualProtect((void*)_tlsHandlerAddress, 16u, 64u, &oldProtect))
{
return;
}
try
{
*(long*)((byte*)_tlsHandlerAddress + 2) = tlsBase;
}
finally
{
VirtualProtect((void*)_tlsHandlerAddress, 16u, oldProtect, &oldProtect);
FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, 16u);
}
}
private unsafe nint CreateImportHandlerTrampoline(int importIndex)
{
void* ptr = VirtualAlloc(null, 192u, 12288u, 64u);
@@ -970,19 +1053,19 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
uint num7 = lpBuffer.Protect & 0xFF;
bool flag = lpBuffer.State == 4096 && (lpBuffer.Protect & PAGE_GUARD) == 0 && num7 != PAGE_NOACCESS;
bool flag2 = num7 == PAGE_EXECUTE || num7 == 32 || num7 == 64 || num7 == PAGE_EXECUTE_WRITECOPY;
if (flag && flag2 && num6 > num5 + (ulong)TlsPattern.Length)
if (flag && flag2 && num6 > num5 + MinTlsPatchInstructionBytes)
{
byte* ptr = (byte*)num5;
int num8 = (int)(num6 - num5) - TlsPattern.Length;
for (int i = 0; i <= num8; i++)
int scanBytes = (int)(num6 - num5);
for (int i = 0; i <= scanBytes - MinTlsPatchInstructionBytes; i++)
{
nint address = (nint)(ptr + i);
if (IsPatternMatch(ptr + i, TlsPattern))
int remainingBytes = scanBytes - i;
if (TryPatchTlsLoadInstruction(address, ptr + i, remainingBytes))
{
num3++;
PatchTlsInstruction(address);
}
else if (TryPatchTlsImmediateStoreInstruction(address, ptr + i))
else if (remainingBytes >= 12 && TryPatchTlsImmediateStoreInstruction(address, ptr + i))
{
num9++;
}
@@ -1075,12 +1158,71 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return true;
}
private unsafe void PatchTlsInstruction(nint address)
private unsafe bool TryPatchTlsLoadInstruction(nint address, byte* source, int availableLength)
{
if (availableLength < MinTlsPatchInstructionBytes)
{
return false;
}
var offset = 0;
while (offset < availableLength && source[offset] == 0x66)
{
offset++;
}
if (offset >= availableLength || source[offset] != 0x64)
{
return false;
}
offset++;
if (offset >= availableLength)
{
return false;
}
var rex = (byte)0;
if (source[offset] >= 0x40 && source[offset] <= 0x4F)
{
rex = source[offset];
offset++;
}
if (offset + 7 > availableLength || source[offset] != 0x8B)
{
return false;
}
var modRm = source[offset + 1];
var sib = source[offset + 2];
if ((modRm >> 6) != 0 || (modRm & 7) != 4 || sib != 0x25)
{
return false;
}
var displacement = *(int*)(source + offset + 3);
if (displacement != 0)
{
return false;
}
var destinationRegister = ((modRm >> 3) & 7) | (((rex & 4) != 0) ? 8 : 0);
var instructionLength = offset + 7;
if (instructionLength < MinTlsPatchInstructionBytes)
{
return false;
}
return PatchTlsLoadInstruction(address, instructionLength, destinationRegister);
}
private unsafe bool PatchTlsLoadInstruction(nint address, int instructionLength, int destinationRegister)
{
uint flNewProtect = default(uint);
if (!VirtualProtect((void*)address, 9u, 64u, &flNewProtect))
if (!VirtualProtect((void*)address, (nuint)instructionLength, 64u, &flNewProtect))
{
return;
return false;
}
try
{
@@ -1091,20 +1233,29 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
if (num3 < int.MinValue || num3 > int.MaxValue)
{
Console.Error.WriteLine($"[LOADER][WARNING] TLS patch out of rel32 range at 0x{address:X16}");
return false;
}
else
*(int*)(address + 1) = (int)num3;
var offset = 5;
if (destinationRegister != 0)
{
*(int*)(address + 1) = (int)num3;
*(sbyte*)(address + 5) = 72;
*(sbyte*)(address + 6) = -119;
*(sbyte*)(address + 7) = -64;
*(sbyte*)(address + 8) = -112;
*(byte*)(address + offset++) = (byte)(0x48 | (destinationRegister >= 8 ? 1 : 0));
*(byte*)(address + offset++) = 0x89;
*(byte*)(address + offset++) = (byte)(0xC0 | (destinationRegister & 7));
}
while (offset < instructionLength)
{
*(byte*)(address + offset++) = 0x90;
}
return true;
}
finally
{
VirtualProtect((void*)address, 9u, flNewProtect, &flNewProtect);
FlushInstructionCache(GetCurrentProcess(), (void*)address, 9u);
VirtualProtect((void*)address, (nuint)instructionLength, flNewProtect, &flNewProtect);
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)instructionLength);
}
}
@@ -1272,6 +1423,439 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
}
}
public bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error)
{
error = null;
if (request.ThreadHandle == 0 || request.EntryPoint < 65536)
{
error = $"invalid thread start request: handle=0x{request.ThreadHandle:X16} entry=0x{request.EntryPoint:X16}";
return false;
}
if (!TryCreateGuestThreadState(creatorContext, request, out var thread, out error))
{
return false;
}
lock (_guestThreadGate)
{
_guestThreads[request.ThreadHandle] = thread;
_readyGuestThreads.Enqueue(thread);
}
Console.Error.WriteLine(
$"[LOADER][INFO] Scheduled guest thread '{thread.Name}' handle=0x{thread.ThreadHandle:X16} entry=0x{thread.EntryPoint:X16} arg=0x{thread.Argument:X16}");
return true;
}
public void Pump(CpuContext callerContext, string reason)
{
_ = callerContext;
if (_guestThreadPumpDepth != 0)
{
return;
}
_guestThreadPumpDepth++;
try
{
for (int i = 0; i < 8; i++)
{
GuestThreadState? thread = null;
lock (_guestThreadGate)
{
while (_readyGuestThreads.Count > 0)
{
var candidate = _readyGuestThreads.Dequeue();
if (candidate.State == GuestThreadRunState.Ready)
{
thread = candidate;
thread.State = GuestThreadRunState.Running;
break;
}
}
}
if (thread == null)
{
return;
}
RunGuestThread(thread, reason);
}
}
finally
{
_guestThreadPumpDepth--;
}
}
private void ClearGuestThreads()
{
lock (_guestThreadGate)
{
_readyGuestThreads.Clear();
_guestThreads.Clear();
}
}
private bool TryCreateGuestThreadState(CpuContext creatorContext, GuestThreadStartRequest request, out GuestThreadState thread, out string? error)
{
thread = null!;
if (!TryGetVirtualMemory(creatorContext, out var virtualMemory))
{
error = "creator context memory is not backed by IVirtualMemory";
return false;
}
if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadStackBaseAddress, GuestThreadStackSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out var stackBase, out error))
{
return false;
}
if (!TryMapGuestThreadRegion(virtualMemory, GuestThreadTlsBaseAddress, GuestThreadTlsSize, ProgramHeaderFlags.Read | ProgramHeaderFlags.Write, out var tlsBase, out error))
{
return false;
}
var trackedMemory = new TrackedCpuMemory(virtualMemory);
var context = new CpuContext(trackedMemory, creatorContext.TargetGeneration)
{
Rip = request.EntryPoint,
Rflags = 0x202,
FsBase = tlsBase,
GsBase = tlsBase,
};
context[CpuRegister.Rsp] = stackBase + GuestThreadStackSize - sizeof(ulong);
context[CpuRegister.Rdi] = request.Argument;
context[CpuRegister.Rsi] = 0;
context[CpuRegister.Rdx] = 0;
context[CpuRegister.Rcx] = 0;
context[CpuRegister.R8] = 0;
context[CpuRegister.R9] = 0;
if (!InitializeGuestThreadFrame(context) || !InitializeGuestThreadTls(context, tlsBase))
{
error = "failed to initialize guest thread stack/TLS";
return false;
}
thread = new GuestThreadState
{
ThreadHandle = request.ThreadHandle,
EntryPoint = request.EntryPoint,
Argument = request.Argument,
Name = string.IsNullOrWhiteSpace(request.Name) ? $"Thread-{request.ThreadHandle:X}" : request.Name,
Context = context,
State = GuestThreadRunState.Ready,
};
error = null;
return true;
}
private static bool TryGetVirtualMemory(CpuContext context, out IVirtualMemory virtualMemory)
{
if (context.Memory is IVirtualMemory directMemory)
{
virtualMemory = directMemory;
return true;
}
if (context.Memory is TrackedCpuMemory trackedMemory && trackedMemory.Inner is IVirtualMemory trackedInner)
{
virtualMemory = trackedInner;
return true;
}
virtualMemory = null!;
return false;
}
private static bool TryMapGuestThreadRegion(
IVirtualMemory virtualMemory,
ulong baseAddress,
ulong size,
ProgramHeaderFlags protection,
out ulong mappedBase,
out string? error)
{
for (int i = 0; i < 64; i++)
{
var candidateBase = baseAddress - ((ulong)i * GuestThreadRegionStride);
if (!IsGuestThreadRegionFree(virtualMemory, candidateBase, size))
{
continue;
}
try
{
virtualMemory.Map(
candidateBase,
size,
fileOffset: 0,
fileData: ReadOnlySpan<byte>.Empty,
protection: protection);
mappedBase = candidateBase;
error = null;
return true;
}
catch (InvalidOperationException)
{
}
}
mappedBase = 0;
error = $"failed to map guest thread region near 0x{baseAddress:X16}";
return false;
}
private static bool IsGuestThreadRegionFree(IVirtualMemory virtualMemory, ulong candidateBase, ulong size)
{
var candidateEnd = candidateBase + size;
foreach (var region in virtualMemory.SnapshotRegions())
{
var regionStart = region.VirtualAddress;
var regionEnd = regionStart + region.MemorySize;
if (candidateBase < regionEnd && regionStart < candidateEnd)
{
return false;
}
}
return true;
}
private static bool InitializeGuestThreadFrame(CpuContext context)
{
var stackTop = context[CpuRegister.Rsp] + sizeof(ulong);
var sentinelFrame = AlignDown(stackTop - 0x20, 16);
var seedRsp = sentinelFrame - sizeof(ulong);
if (!context.TryWriteUInt64(sentinelFrame, 0) ||
!context.TryWriteUInt64(sentinelFrame + sizeof(ulong), 0) ||
!context.TryWriteUInt64(seedRsp, 0))
{
return false;
}
context[CpuRegister.Rbp] = sentinelFrame;
context[CpuRegister.Rsp] = seedRsp;
return true;
}
private static bool InitializeGuestThreadTls(CpuContext context, ulong tlsBase)
{
return context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
context.TryWriteUInt64(tlsBase + 0x10, tlsBase) &&
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL);
}
private void RunGuestThread(GuestThreadState thread, string reason)
{
Console.Error.WriteLine(
$"[LOADER][INFO] Pumping guest thread '{thread.Name}' reason={reason} entry=0x{thread.EntryPoint:X16}");
var previousContext = _cpuContext;
var previousTlsBase = _tlsBaseAddress;
var previousOwnsTlsBase = _ownsTlsBaseAddress;
var previousForcedGuestExit = _forcedGuestExit;
var previousLastError = LastError;
var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(thread.ThreadHandle);
try
{
_cpuContext = thread.Context;
_forcedGuestExit = false;
LastError = null;
BindTlsBase(thread.Context);
var exitReason = ExecuteGuestThreadEntry(thread.Context, thread.EntryPoint, thread.Name, out var blockReason);
lock (_guestThreadGate)
{
switch (exitReason)
{
case GuestNativeCallExitReason.Returned:
thread.State = GuestThreadRunState.Exited;
break;
case GuestNativeCallExitReason.Blocked:
thread.State = GuestThreadRunState.Blocked;
thread.BlockReason = blockReason;
break;
default:
thread.State = GuestThreadRunState.Faulted;
thread.BlockReason = blockReason;
break;
}
}
Console.Error.WriteLine(
$"[LOADER][INFO] Guest thread '{thread.Name}' state={thread.State} reason={blockReason ?? "none"}");
}
finally
{
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
_cpuContext = previousContext;
_tlsBaseAddress = previousTlsBase;
_ownsTlsBaseAddress = previousOwnsTlsBase;
if (_tlsBaseAddress != 0)
{
SeedTlsLayout(_tlsBaseAddress);
UpdateTlsHandlerBase(_tlsBaseAddress);
}
_forcedGuestExit = previousForcedGuestExit;
LastError = previousLastError;
}
}
private unsafe GuestNativeCallExitReason ExecuteGuestThreadEntry(CpuContext context, ulong entryPoint, string name, out string? reason)
{
reason = null;
if (context[CpuRegister.Rsp] == 0)
{
reason = "guest thread stack pointer is zero";
return GuestNativeCallExitReason.Exception;
}
void* ptr = VirtualAlloc(null, 256u, 12288u, 64u);
if (ptr == null)
{
reason = "failed to allocate executable memory for guest thread stub";
return GuestNativeCallExitReason.Exception;
}
var previousSentinel = _entryReturnSentinelRip;
var previousHostRspSlotValue = _hostRspSlotStorage != 0 ? *(ulong*)_hostRspSlotStorage : 0;
var previousYieldRequested = _guestThreadYieldRequested;
var previousYieldReason = _guestThreadYieldReason;
try
{
byte* ptr2 = (byte*)ptr;
ulong hostRspSlot = (ulong)ptr + 224uL;
int offset = 0;
ptr2[offset++] = 83;
ptr2[offset++] = 85;
ptr2[offset++] = 87;
ptr2[offset++] = 86;
ptr2[offset++] = 73;
ptr2[offset++] = 186;
*(ulong*)(ptr2 + offset) = hostRspSlot;
offset += 8;
ptr2[offset++] = 73;
ptr2[offset++] = 137;
ptr2[offset++] = 34;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 196;
ptr2[offset++] = 72;
ptr2[offset++] = 131;
ptr2[offset++] = 236;
ptr2[offset++] = 8;
ptr2[offset++] = 72;
ptr2[offset++] = 189;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 199;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 198;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 194;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx];
offset += 8;
ptr2[offset++] = 72;
ptr2[offset++] = 137;
ptr2[offset++] = 193;
ptr2[offset++] = 72;
ptr2[offset++] = 184;
*(ulong*)(ptr2 + offset) = entryPoint;
offset += 8;
ptr2[offset++] = byte.MaxValue;
ptr2[offset++] = 208;
int sentinelOffset = offset + 4;
ptr2[offset++] = 72;
ptr2[offset++] = 131;
ptr2[offset++] = 196;
ptr2[offset++] = 8;
ptr2[offset++] = 73;
ptr2[offset++] = 186;
*(ulong*)(ptr2 + offset) = hostRspSlot;
offset += 8;
ptr2[offset++] = 73;
ptr2[offset++] = 139;
ptr2[offset++] = 34;
ptr2[offset++] = 94;
ptr2[offset++] = 95;
ptr2[offset++] = 93;
ptr2[offset++] = 91;
ptr2[offset++] = 195;
ulong sentinel = (ulong)ptr + (ulong)sentinelOffset;
_entryReturnSentinelRip = sentinel;
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel))
{
reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}";
return GuestNativeCallExitReason.Exception;
}
uint oldProtect = default(uint);
VirtualProtect(ptr, 256u, 64u, &oldProtect);
FlushInstructionCache(GetCurrentProcess(), ptr, 256u);
if (_hostRspSlotStorage != 0)
{
*(ulong*)_hostRspSlotStorage = hostRspSlot;
}
_guestThreadYieldRequested = false;
_guestThreadYieldReason = null;
try
{
var nativeReturn = Marshal.GetDelegateForFunctionPointer<NativeEntryDelegate>((nint)ptr)();
if (_guestThreadYieldRequested)
{
reason = _guestThreadYieldReason ?? "guest thread blocked";
return GuestNativeCallExitReason.Blocked;
}
if (_forcedGuestExit)
{
reason = LastError ?? "guest thread forced exit";
return GuestNativeCallExitReason.ForcedExit;
}
reason = $"returned 0x{nativeReturn:X8}";
return GuestNativeCallExitReason.Returned;
}
catch (AccessViolationException ex)
{
reason = "access violation: " + ex.Message;
return GuestNativeCallExitReason.Exception;
}
catch (Exception ex)
{
reason = ex.GetType().Name + ": " + ex.Message;
return GuestNativeCallExitReason.Exception;
}
}
finally
{
_entryReturnSentinelRip = previousSentinel;
if (_hostRspSlotStorage != 0)
{
*(ulong*)_hostRspSlotStorage = previousHostRspSlotValue;
}
_guestThreadYieldRequested = previousYieldRequested;
_guestThreadYieldReason = previousYieldReason;
VirtualFree(ptr, 0u, 32768u);
}
}
private static ulong AlignDown(ulong value, ulong alignment)
{
if (alignment == 0)
{
return value;
}
return value & ~(alignment - 1);
}
private unsafe bool ExecuteEntry(CpuContext context, ulong entryPoint, out OrbisGen2Result result)
{
Console.Error.WriteLine($"[LOADER][INFO] ExecuteEntry starting at 0x{entryPoint:X16}");
@@ -1319,6 +1903,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[num3++] = 236;
ptr2[num3++] = 8;
ptr2[num3++] = 72;
ptr2[num3++] = 189;
*(ulong*)(ptr2 + num3) = context[CpuRegister.Rbp];
num3 += 8;
ptr2[num3++] = 72;
ptr2[num3++] = 184;
*(ulong*)(ptr2 + num3) = context[CpuRegister.Rdi];
num3 += 8;
@@ -1397,6 +1985,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
{
num6 = Marshal.GetDelegateForFunctionPointer<NativeEntryDelegate>((nint)ptr)();
Console.Error.WriteLine($"[LOADER][INFO] Guest returned: {num6}");
Pump(context, "entry_return");
}
catch (AccessViolationException ex)
{
@@ -1614,11 +2203,13 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
_selfHandle.Free();
_selfHandlePtr = 0;
}
if (_ownsTlsBaseAddress && _tlsBaseAddress != 0)
if (_ownedTlsBaseAddress != 0)
{
VirtualFree((void*)_tlsBaseAddress, 0u, 32768u);
VirtualFree((void*)_ownedTlsBaseAddress, 0u, 32768u);
_ownedTlsBaseAddress = 0;
}
_tlsBaseAddress = 0;
_ownsTlsBaseAddress = false;
if (_tlsModuleBases.Count > 0)
{
foreach (var (_, num3) in _tlsModuleBases)
@@ -40,6 +40,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool VirtualProtect(void* lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
[DllImport("kernel32.dll")]
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
[DllImport("kernel32.dll")]
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
@@ -351,6 +354,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return true;
}
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
return false;
}
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
{
return false;
@@ -389,6 +397,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return true;
}
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
return false;
}
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
{
return false;
@@ -494,6 +507,48 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
return protection is PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY;
}
private static uint GetCommitProtection(MemoryRegion region)
{
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
}
private static unsafe bool EnsureRangeCommitted(ulong address, ulong size, MemoryRegion region)
{
if (size == 0 || !region.IsReservedOnly)
{
return true;
}
var startPage = AlignDown(address, PageSize);
var endPage = AlignUp(address + size, PageSize);
var commitProtection = GetCommitProtection(region);
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
{
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
{
return false;
}
if (info.State == MEM_COMMIT)
{
continue;
}
if (info.State != MEM_RESERVE)
{
return false;
}
if (VirtualAlloc((void*)pageAddress, (nuint)PageSize, MEM_COMMIT, commitProtection) == null)
{
return false;
}
}
return true;
}
private bool TryTemporarilyProtectForRead(
ulong address,
ulong size,
@@ -558,4 +613,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
public bool IsReservedOnly { get; set; }
public uint Protection { get; set; }
}
private struct MemoryBasicInformation64
{
public ulong BaseAddress;
public ulong AllocationBase;
public uint AllocationProtect;
public uint Alignment1;
public ulong RegionSize;
public uint State;
public uint Protect;
public uint Type;
public uint Alignment2;
}
}
+38 -2
View File
@@ -125,6 +125,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
public OrbisGen2Result Run(string ebootPath)
{
var normalizedEbootPath = Path.GetFullPath(ebootPath);
using var app0Binding = BindApp0Root(normalizedEbootPath);
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
LastExecutionDiagnostics = null;
LastExecutionTrace = null;
@@ -132,8 +134,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
LastBasicBlockTrace = null;
LastMilestoneLog = null;
KernelModuleRegistry.Reset();
var image = LoadImage(ebootPath);
var normalizedEbootPath = Path.GetFullPath(ebootPath);
var image = LoadImage(normalizedEbootPath);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
@@ -350,6 +351,41 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return result;
}
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
{
const string app0VariableName = "SHARPEMU_APP0_DIR";
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable(app0VariableName)))
{
return null;
}
var app0Root = Path.GetDirectoryName(normalizedEbootPath);
if (string.IsNullOrWhiteSpace(app0Root))
{
return null;
}
Environment.SetEnvironmentVariable(app0VariableName, app0Root);
return new App0BindingScope(app0VariableName);
}
private sealed class App0BindingScope(string variableName) : IDisposable
{
private readonly string _variableName = variableName;
private bool _disposed;
public void Dispose()
{
if (_disposed)
{
return;
}
Environment.SetEnvironmentVariable(_variableName, null);
_disposed = true;
}
}
private OrbisGen2Result? RunAllInitializers(
SelfImage mainImage,
IReadOnlyList<LoadedModuleImage> loadedModuleImages,
+107
View File
@@ -0,0 +1,107 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE;
public readonly record struct GuestThreadStartRequest(
ulong ThreadHandle,
ulong EntryPoint,
ulong Argument,
ulong AttributeAddress,
string Name);
public interface IGuestThreadScheduler
{
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
void Pump(CpuContext callerContext, string reason);
}
public static class GuestThreadExecution
{
[ThreadStatic]
private static ulong _currentGuestThreadHandle;
[ThreadStatic]
private static string? _pendingBlockReason;
[ThreadStatic]
private static bool _pendingEntryExit;
[ThreadStatic]
private static int _pendingEntryExitStatus;
[ThreadStatic]
private static string? _pendingEntryExitReason;
public static IGuestThreadScheduler? Scheduler { get; set; }
public static bool IsGuestThread => _currentGuestThreadHandle != 0;
public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle;
public static ulong EnterGuestThread(ulong threadHandle)
{
var previous = _currentGuestThreadHandle;
_currentGuestThreadHandle = threadHandle;
_pendingBlockReason = null;
_pendingEntryExit = false;
_pendingEntryExitStatus = 0;
_pendingEntryExitReason = null;
return previous;
}
public static void RestoreGuestThread(ulong previousThreadHandle)
{
_currentGuestThreadHandle = previousThreadHandle;
_pendingBlockReason = null;
_pendingEntryExit = false;
_pendingEntryExitStatus = 0;
_pendingEntryExitReason = null;
}
public static bool RequestCurrentThreadBlock(string reason)
{
if (!IsGuestThread)
{
return false;
}
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
return true;
}
public static bool TryConsumeCurrentThreadBlock(out string reason)
{
reason = _pendingBlockReason ?? string.Empty;
if (string.IsNullOrEmpty(reason))
{
return false;
}
_pendingBlockReason = null;
return true;
}
public static void RequestCurrentEntryExit(string reason, int status)
{
_pendingEntryExit = true;
_pendingEntryExitStatus = status;
_pendingEntryExitReason = string.IsNullOrWhiteSpace(reason) ? "guest_entry_exit" : reason;
}
public static bool TryConsumeCurrentEntryExit(out int status, out string reason)
{
status = _pendingEntryExitStatus;
reason = _pendingEntryExitReason ?? string.Empty;
if (!_pendingEntryExit)
{
return false;
}
_pendingEntryExit = false;
_pendingEntryExitStatus = 0;
_pendingEntryExitReason = null;
return true;
}
}
+641
View File
@@ -0,0 +1,641 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
namespace SharpEmu.Libs.Ampr;
public static class AmprExports
{
private const int CommandBufferHeaderSize = 0x28;
private const ulong CommandBufferSelfOffset = 0x00;
private const ulong CommandBufferDataOffset = 0x08;
private const ulong CommandBufferSizeOffset = 0x10;
private const ulong CommandBufferAux0Offset = 0x18;
private const ulong CommandBufferAux1Offset = 0x20;
private const ulong ReadFileRecordSize = 0x30;
private const ulong KernelEventQueueRecordSize = 0x30;
private const uint ReadFileRecordType = 1;
private const uint KernelEventQueueRecordType = 2;
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
private sealed class CommandBufferState
{
public ulong Buffer;
public ulong Size;
public ulong WriteOffset;
}
[SysAbiExport(
Nid = "8aI7R7WaOlc",
ExportName = "sceAmprCommandBufferConstructor",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferConstructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var buffer = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0: 0, aux1: 0, clear: true))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "ctor", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = commandBuffer;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "a8uLzYY--tM",
ExportName = "sceAmprAprCommandBufferConstructor",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int AprCommandBufferConstructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var aux0 = ctx[CpuRegister.Rsi];
var aux1 = ctx[CpuRegister.Rdx];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var buffer = 0UL;
var size = 0UL;
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer);
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size);
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0, aux1, clear: false))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "apr_ctor", commandBuffer, aux0, aux1);
ctx[CpuRegister.Rax] = commandBuffer;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Qs1xtplKo0U",
ExportName = "sceAmprAprCommandBufferDestructor",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int AprCommandBufferDestructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, 0) ||
!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "apr_dtor", commandBuffer, 0, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "GuchCTefuZw",
ExportName = "sceAmprCommandBufferDestructor",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferDestructor(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer: 0, size: 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
_commandBuffers.TryRemove(commandBuffer, out _);
TraceAmpr(ctx, "dtor", commandBuffer, 0, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "N-FSPA4S3nI",
ExportName = "sceAmprCommandBufferSetBuffer",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferSetBuffer(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var buffer = ctx[CpuRegister.Rsi];
var size = ctx[CpuRegister.Rdx];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "set_buffer", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "baQO9ez2gL4",
ExportName = "sceAmprCommandBufferReset",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferReset(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out var buffer) ||
!ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out var size) ||
!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "reset", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "ULvXMDz56po",
ExportName = "sceAmprCommandBufferClearBuffer",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferClearBuffer(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out var buffer, out var size, out _) ||
!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer: 0, size: 0))
{
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
_commandBuffers.TryRemove(commandBuffer, out _);
TraceAmpr(ctx, "clear_buffer", commandBuffer, buffer, size);
ctx[CpuRegister.Rax] = buffer;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "mQ16-QdKv7k",
ExportName = "sceAmprAprCommandBufferReadFile",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int AprCommandBufferReadFile(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var fileId = unchecked((uint)ctx[CpuRegister.Rcx]);
var destination = ctx[CpuRegister.R8];
var size = ctx[CpuRegister.R9];
if (commandBuffer == 0 || (destination == 0 && size != 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out var fileOffset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
{
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
{
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
return result;
}
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_OK);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "vWU-odnS+fU",
ExportName = "sceAmprMeasureCommandSizeReadFile",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeReadFile(CpuContext ctx)
{
TraceAmpr(ctx, "measure_read_file", 0, ReadFileRecordSize, 0);
ctx[CpuRegister.Rax] = ReadFileRecordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "sSAUCCU1dv4",
ExportName = "sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int MeasureCommandSizeWriteKernelEventQueue0400(CpuContext ctx)
{
TraceAmpr(ctx, "measure_write_equeue", 0, KernelEventQueueRecordSize, 0);
ctx[CpuRegister.Rax] = KernelEventQueueRecordSize;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "tZDDEo2tE5k",
ExportName = "sceAmprCommandBufferGetSize",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferGetSize(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out var size, out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "get_size", commandBuffer, size, 0);
ctx[CpuRegister.Rax] = size;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "GnxKOHEawhk",
ExportName = "sceAmprCommandBufferGetCurrentOffset",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferGetCurrentOffset(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryGetCommandBufferOffset(ctx, commandBuffer, out var offset))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "get_offset", commandBuffer, offset, 0);
ctx[CpuRegister.Rax] = offset;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "H896Pt-yB4I",
ExportName = "sceAmprCommandBufferWriteKernelEventQueue_04_00",
Target = Generation.Gen5,
LibraryName = "libSceAmpr")]
public static int CommandBufferWriteKernelEventQueue0400(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var equeue = ctx[CpuRegister.Rsi];
var ident = ctx[CpuRegister.Rdx];
var filter = ctx[CpuRegister.Rcx];
var userData = ctx[CpuRegister.R8];
var data = ctx[CpuRegister.R9];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var extra = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out extra);
if (!AppendKernelEventQueueRecord(ctx, commandBuffer, equeue, ident, filter, userData, data, extra))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceAmpr(ctx, "write_equeue", commandBuffer, equeue, ident);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool InitializeCommandBuffer(
CpuContext ctx,
ulong commandBuffer,
ulong buffer,
ulong size,
ulong aux0,
ulong aux1,
bool clear)
{
if (clear)
{
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
header.Clear();
if (!ctx.Memory.TryWrite(commandBuffer, header))
{
return false;
}
}
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, aux0) &&
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, aux1) &&
WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0);
}
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
{
return WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0);
}
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size, ulong writeOffset)
{
if (!WriteVisibleCommandBufferPointers(ctx, commandBuffer, buffer, size))
{
return false;
}
var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
lock (state)
{
state.Buffer = buffer;
state.Size = size;
state.WriteOffset = writeOffset;
}
return true;
}
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
{
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
ctx.TryWriteUInt64(commandBuffer + CommandBufferDataOffset, buffer) &&
ctx.TryWriteUInt64(commandBuffer + CommandBufferSizeOffset, size);
}
private static bool TryGetCommandBufferState(
CpuContext ctx,
ulong commandBuffer,
out ulong buffer,
out ulong size,
out CommandBufferState? state)
{
if (_commandBuffers.TryGetValue(commandBuffer, out state))
{
lock (state)
{
buffer = state.Buffer;
size = state.Size;
}
return true;
}
if (ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer) &&
ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size))
{
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
lock (state)
{
state.Buffer = buffer;
state.Size = size;
state.WriteOffset = 0;
}
return true;
}
buffer = 0;
size = 0;
state = null;
return false;
}
private static bool TryGetCommandBufferOffset(CpuContext ctx, ulong commandBuffer, out ulong offset)
{
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
{
offset = 0;
return false;
}
lock (state)
{
offset = state.WriteOffset;
}
return true;
}
private static int TryReadFileToGuestMemory(
CpuContext ctx,
string hostPath,
ulong fileOffset,
ulong destination,
ulong size,
out ulong bytesRead)
{
bytesRead = 0;
if (size == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (fileOffset > long.MaxValue)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
const int ChunkSize = 64 * 1024;
var buffer = new byte[(int)Math.Min((ulong)ChunkSize, size)];
try
{
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
if (fileOffset >= (ulong)stream.Length)
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
stream.Position = unchecked((long)fileOffset);
while (bytesRead < size)
{
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
var read = stream.Read(buffer, 0, request);
if (read <= 0)
{
break;
}
if (!ctx.Memory.TryWrite(destination + bytesRead, buffer.AsSpan(0, read)))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
bytesRead += (ulong)read;
}
}
catch (UnauthorizedAccessException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
catch (IOException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool AppendReadFileRecord(
CpuContext ctx,
ulong commandBuffer,
uint fileId,
ulong destination,
ulong size,
ulong fileOffset,
ulong bytesRead)
{
Span<byte> record = stackalloc byte[(int)ReadFileRecordSize];
record.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], ReadFileRecordType);
BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], fileId);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], destination);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], size);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], fileOffset);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], bytesRead);
return AppendCommandBufferRecord(ctx, commandBuffer, record);
}
private static bool AppendKernelEventQueueRecord(
CpuContext ctx,
ulong commandBuffer,
ulong equeue,
ulong ident,
ulong filter,
ulong userData,
ulong data,
ulong extra)
{
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
record.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType);
BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], unchecked((uint)filter));
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], equeue);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], data);
BinaryPrimitives.WriteUInt64LittleEndian(record[0x28..], extra);
return AppendCommandBufferRecord(ctx, commandBuffer, record);
}
private static bool AppendCommandBufferRecord(CpuContext ctx, ulong commandBuffer, ReadOnlySpan<byte> record)
{
if (!TryGetCommandBufferState(ctx, commandBuffer, out _, out _, out var state) || state is null)
{
return false;
}
var recordSize = (ulong)record.Length;
lock (state)
{
if (state.Buffer == 0 ||
state.WriteOffset > state.Size ||
recordSize > state.Size - state.WriteOffset)
{
return false;
}
if (!ctx.Memory.TryWrite(state.Buffer + state.WriteOffset, record))
{
return false;
}
state.WriteOffset += recordSize;
}
return true;
}
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
{
return;
}
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] ampr.{operation}: cmd=0x{commandBuffer:X16} arg0=0x{arg0:X16} arg1=0x{arg1:X16} ret=0x{returnRip:X16}");
}
private static void TraceAmprRead(
CpuContext ctx,
ulong commandBuffer,
uint fileId,
ulong destination,
ulong size,
ulong fileOffset,
ulong bytesRead,
string? hostPath,
int result)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
{
return;
}
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] ampr.read_file: cmd=0x{commandBuffer:X16} id=0x{fileId:X8} dst=0x{destination:X16} size=0x{size:X16} offset=0x{fileOffset:X16} read=0x{bytesRead:X16} result=0x{result:X8} path='{hostPath ?? string.Empty}' ret=0x{returnRip:X16}");
}
}
@@ -0,0 +1,40 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
namespace SharpEmu.Libs.Ampr;
internal static class AmprFileRegistry
{
private static readonly ConcurrentDictionary<uint, string> _hostPathsById = new();
public static uint Register(string guestPath, string hostPath)
{
var id = ComputeFileId(guestPath);
_hostPathsById[id] = hostPath;
return id;
}
public static bool TryGetHostPath(uint id, out string hostPath)
{
return _hostPathsById.TryGetValue(id, out hostPath!);
}
private static uint ComputeFileId(string guestPath)
{
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
const uint offsetBasis = 2166136261;
const uint prime = 16777619;
var hash = offsetBasis;
foreach (var b in bytes)
{
hash ^= b;
hash *= prime;
}
return hash;
}
}
@@ -2,11 +2,41 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
namespace SharpEmu.Libs.AppContent;
public static class AppContentExports
{
private const ulong BootParamAttrOffset = 4;
private const string Temp0MountPoint = "/temp0";
[SysAbiExport(
Nid = "R9lA82OraNs",
ExportName = "sceAppContentInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentInitialize(CpuContext ctx)
{
var initParamAddress = ctx[CpuRegister.Rdi];
var bootParamAddress = ctx[CpuRegister.Rsi];
if (initParamAddress == 0 || bootParamAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Span<byte> attrBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(attrBytes, 0);
if (!ctx.Memory.TryWrite(bootParamAddress + BootParamAttrOffset, attrBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "xnd8BJzAxmk",
ExportName = "sceAppContentGetAddcontInfoList",
@@ -14,7 +44,67 @@ public static class AppContentExports
LibraryName = "libSceAppContent")]
public static int AppContentGetAddcontInfoList(CpuContext ctx)
{
_ = ctx;
var hitCountAddress = ctx[CpuRegister.Rcx];
if (hitCountAddress != 0)
{
Span<byte> hitCountBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(hitCountBytes, 0);
if (!ctx.Memory.TryWrite(hitCountAddress, hitCountBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "buYbeLOGWmA",
ExportName = "sceAppContentTemporaryDataMount2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentTemporaryDataMount2(CpuContext ctx)
{
var mountPointAddress = ctx[CpuRegister.Rsi];
if (mountPointAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Directory.CreateDirectory(ResolveTemp0Root());
var mountPointBytes = Encoding.ASCII.GetBytes($"{Temp0MountPoint}\0");
if (!ctx.Memory.TryWrite(mountPointAddress, mountPointBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static string ResolveTemp0Root()
{
const string temp0VariableName = "SHARPEMU_TEMP0_DIR";
var configuredRoot = Environment.GetEnvironmentVariable(temp0VariableName);
if (!string.IsNullOrWhiteSpace(configuredRoot))
{
return Path.GetFullPath(configuredRoot);
}
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
var appName = string.IsNullOrWhiteSpace(app0Root)
? "default"
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
if (string.IsNullOrWhiteSpace(appName))
{
appName = "default";
}
var invalidChars = Path.GetInvalidFileNameChars();
appName = new string(appName.Select(ch => invalidChars.Contains(ch) ? '_' : ch).ToArray());
var root = Path.Combine(Path.GetTempPath(), "SharpEmu", appName, "temp0");
Environment.SetEnvironmentVariable(temp0VariableName, root);
return root;
}
}
@@ -0,0 +1,158 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Kernel;
public static class KernelAprCompatExports
{
private static readonly ConcurrentDictionary<uint, ulong> _submittedCommandBuffers = new();
private static int _nextSubmissionId;
[SysAbiExport(
Nid = "ASoW5WE-UPo",
ExportName = "sceKernelAprSubmitCommandBufferAndGetResult",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprSubmitCommandBufferAndGetResult(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var priority = ctx[CpuRegister.Rsi];
var resultAddress = ctx[CpuRegister.Rdx];
var outSubmissionId = ctx[CpuRegister.Rcx];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
if (submissionId == 0)
{
submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
}
_submittedCommandBuffers[submissionId] = commandBuffer;
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceApr(ctx, "submit_get_result", submissionId, commandBuffer, priority, resultAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "rqwFKI4PAiM",
ExportName = "sceKernelAprWaitCommandBuffer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprWaitCommandBuffer(CpuContext ctx)
{
var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]);
var priority = ctx[CpuRegister.Rsi];
var resultAddress = ctx[CpuRegister.Rdx];
if (!_submittedCommandBuffers.TryRemove(submissionId, out var commandBuffer))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceApr(ctx, "wait", submissionId, commandBuffer, priority, resultAddress);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "eE4Szl8sil8",
ExportName = "sceKernelAprSubmitCommandBuffer",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprSubmitCommandBuffer(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
if (commandBuffer == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
_submittedCommandBuffers[submissionId] = commandBuffer;
TraceApr(ctx, "submit", submissionId, commandBuffer, ctx[CpuRegister.Rsi], 0);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "qvMUCyyaCSI",
ExportName = "sceKernelAprSubmitCommandBufferAndGetId",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAprSubmitCommandBufferAndGetId(CpuContext ctx)
{
var commandBuffer = ctx[CpuRegister.Rdi];
var outSubmissionId = ctx[CpuRegister.Rdx];
if (commandBuffer == 0 || outSubmissionId == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
_submittedCommandBuffers[submissionId] = commandBuffer;
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceApr(ctx, "submit_get_id", submissionId, commandBuffer, ctx[CpuRegister.Rsi], outSubmissionId);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress)
{
Span<byte> result = stackalloc byte[sizeof(ulong)];
result.Clear();
return ctx.Memory.TryWrite(resultAddress, result);
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static void TraceApr(
CpuContext ctx,
string operation,
uint submissionId,
ulong commandBuffer,
ulong priority,
ulong aux)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
{
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} priority=0x{priority:X16} aux=0x{aux:X16} ret=0x{returnRip:X16}");
}
}
@@ -0,0 +1,204 @@
// 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.Kernel;
public static class KernelEventQueueCompatExports
{
private static readonly object _eventQueueGate = new();
private static readonly HashSet<ulong> _eventQueues = new();
private static long _nextEventQueueHandle = 1;
[SysAbiExport(
Nid = "D0OdFMjp46I",
ExportName = "sceKernelCreateEqueue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelCreateEqueue(CpuContext ctx)
{
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventQueueHandle));
lock (_eventQueueGate)
{
_eventQueues.Add(handle);
}
if (!ctx.TryWriteUInt64(outAddress, handle))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
TraceEventQueue(ctx, "create", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "jpFjmgAC5AE",
ExportName = "sceKernelDeleteEqueue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelDeleteEqueue(CpuContext ctx)
{
var handle = ctx[CpuRegister.Rdi];
lock (_eventQueueGate)
{
_eventQueues.Remove(handle);
}
TraceEventQueue(ctx, "delete", handle);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "WDszmSbWuDk",
ExportName = "sceKernelAddUserEventEdge",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAddUserEventEdge(CpuContext ctx)
{
TraceEventQueue(ctx, "add_user_edge", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "4R6-OvI2cEA",
ExportName = "sceKernelAddUserEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAddUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "add_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "LJDwdSNTnDg",
ExportName = "sceKernelDeleteUserEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelDeleteUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "delete_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "F6e0kwo4cnk",
ExportName = "sceKernelTriggerUserEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelTriggerUserEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "trigger_user", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "bBfz7kMF2Ho",
ExportName = "sceKernelAddAmprEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAddAmprEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "add_ampr", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "vuae5JPNt9A",
ExportName = "sceKernelAddAmprSystemEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAddAmprSystemEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "add_ampr_system", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "bMmid3pfyjo",
ExportName = "sceKernelDeleteAmprEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelDeleteAmprEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "delete_ampr", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Ij+ryuEClXQ",
ExportName = "sceKernelDeleteAmprSystemEvent",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelDeleteAmprSystemEvent(CpuContext ctx)
{
TraceEventQueue(ctx, "delete_ampr_system", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "QyrxcdBrb0M",
ExportName = "sceKernelGetKqueueFromEqueue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelGetKqueueFromEqueue(CpuContext ctx)
{
ctx[CpuRegister.Rax] = ctx[CpuRegister.Rdi];
TraceEventQueue(ctx, "get_kqueue", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "fzyMKs9kim0",
ExportName = "sceKernelWaitEqueue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelWaitEqueue(CpuContext ctx)
{
var outCountAddress = ctx[CpuRegister.Rcx];
var timeoutAddress = ctx[CpuRegister.R8];
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, 0))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue"))
{
TraceEventQueue(ctx, "wait-block", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
TraceEventQueue(ctx, "wait", ctx[CpuRegister.Rdi]);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal))
{
return;
}
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
}
+38 -86
View File
@@ -55,7 +55,10 @@ public static class KernelExports
LibraryName = "libKernel")]
public static int Exit(CpuContext ctx)
{
_ = ctx;
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -68,7 +71,8 @@ public static class KernelExports
{
var status = unchecked((int)ctx[CpuRegister.Rdi]);
Console.Error.WriteLine($"[LOADER][INFO] catchReturnFromMain(status={status})");
ctx[CpuRegister.Rax] = 0;
GuestThreadExecution.RequestCurrentEntryExit("catchReturnFromMain", status);
ctx[CpuRegister.Rax] = unchecked((ulong)status);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -211,6 +215,19 @@ public static class KernelExports
$"[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}");
}
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is not null && entryAddress != 0)
{
var request = new GuestThreadStartRequest(threadHandle, entryAddress, argument, attrAddress, name);
if (!scheduler.TryStartThread(ctx, request, out var error))
{
Console.Error.WriteLine(
$"[LOADER][ERROR] pthread_create: failed to schedule guest thread '{name}' entry=0x{entryAddress:X16}: {error}");
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -225,6 +242,16 @@ public static class KernelExports
return PthreadCreate(ctx);
}
[SysAbiExport(
Nid = "Jmi+9w9u0E4",
ExportName = "pthread_create_name_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCreateNameNp(CpuContext ctx)
{
return PthreadCreate(ctx);
}
[SysAbiExport(
Nid = "onNY9Byn-W8",
ExportName = "scePthreadJoin",
@@ -264,22 +291,14 @@ public static class KernelExports
ExportName = "open",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int Open(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)Interlocked.Increment(ref _nextFileDescriptor));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static int Open(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
[SysAbiExport(
Nid = "1G3lF1Gg1k8",
ExportName = "sceKernelOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelOpen(CpuContext ctx)
{
ctx[CpuRegister.Rax] = unchecked((ulong)Interlocked.Increment(ref _nextFileDescriptor));
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
[SysAbiExport(
Nid = "hcuQgD53UxM",
@@ -290,84 +309,17 @@ public static class KernelExports
{
ulong fmtPtr = ctx[CpuRegister.Rdi];
string fmt = ReadCString(ctx, fmtPtr, 4096);
ulong[] args =
string outStr = KernelMemoryCompatExports.FormatStringFromVarArgs(ctx, fmt, firstGpArgIndex: 1);
if (outStr.EndsWith('\n') || outStr.EndsWith('\r'))
{
ctx[CpuRegister.Rsi],
ctx[CpuRegister.Rdx],
ctx[CpuRegister.Rcx],
ctx[CpuRegister.R8],
ctx[CpuRegister.R9],
};
int argIndex = 0;
var sb = new System.Text.StringBuilder(fmt.Length + 64);
for (int i = 0; i < fmt.Length; i++)
Console.Write($"[DEBUG][PRINF] {outStr}");
}
else
{
char ch = fmt[i];
if (ch != '%')
{
sb.Append(ch);
continue;
}
if (i + 1 < fmt.Length && fmt[i + 1] == '%')
{
sb.Append('%');
i++;
continue;
}
int j = i + 1;
while (j < fmt.Length && "-+ #0".IndexOf(fmt[j]) >= 0) j++;
while (j < fmt.Length && char.IsDigit(fmt[j])) j++;
if (j < fmt.Length && fmt[j] == '.')
{
j++;
while (j < fmt.Length && char.IsDigit(fmt[j])) j++;
}
if (j >= fmt.Length) break;
char spec = fmt[j];
i = j;
ulong a = argIndex < args.Length ? args[argIndex++] : 0;
switch (spec)
{
case 's':
sb.Append(a == 0 ? "(null)" : ReadCString(ctx, a, 4096));
break;
case 'd':
case 'i':
sb.Append(unchecked((int)a));
break;
case 'u':
sb.Append(unchecked((uint)a));
break;
case 'x':
sb.Append(unchecked((uint)a).ToString("x"));
break;
case 'p':
sb.Append("0x").Append(a.ToString("x16"));
break;
default:
sb.Append('%').Append(spec);
break;
}
Console.WriteLine($"[DEBUG][PRINF] {outStr}");
}
string outStr = sb.ToString();
Console.WriteLine($"[DEBUG][PRINF] {outStr}");
ctx[CpuRegister.Rax] = (ulong)outStr.Length;
ctx[CpuRegister.Rax] = (ulong)System.Text.Encoding.UTF8.GetByteCount(outStr);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
File diff suppressed because it is too large Load Diff
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
@@ -11,10 +12,13 @@ public static class KernelPthreadCompatExports
private const int MutexTypeDefault = 1;
private const int MutexTypeErrorCheck = 1;
private const int MutexTypeRecursive = 2;
private const int MutexTypeNormal = 4;
private const int MutexTypeNormal = 3;
private const int MutexTypeAdaptiveNp = 4;
private const ulong StaticAdaptiveMutexInitializer = 1;
private const ulong SyntheticMutexHandleBase = 0x00006000_0000_0000;
private const ulong SyntheticMutexAttrHandleBase = 0x00006001_0000_0000;
private const ulong SyntheticCondHandleBase = 0x00006002_0000_0000;
private const int DefaultSpuriousCondWakeMilliseconds = 1;
private static readonly object _stateGate = new();
private static readonly Dictionary<ulong, PthreadMutexState> _mutexStates = new();
@@ -86,18 +90,14 @@ public static class KernelPthreadCompatExports
ExportName = "scePthreadGetthreadid",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadGetthreadid(CpuContext ctx)
{
var currentThreadId = KernelPthreadState.GetCurrentThreadUniqueId();
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress != 0 && !ctx.TryWriteUInt64(outAddress, currentThreadId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
public static int PthreadGetthreadid(CpuContext ctx) => PthreadGetthreadidCore(ctx);
ctx[CpuRegister.Rax] = currentThreadId;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "3eqs37G74-s",
ExportName = "pthread_getthreadid_np",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadGetthreadidNp(CpuContext ctx) => PthreadGetthreadidCore(ctx);
[SysAbiExport(
Nid = "cmo1RIYva9o",
@@ -169,6 +169,12 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PosixPthreadMutexUnlock(CpuContext ctx) => PthreadMutexUnlockCore(ctx, ctx[CpuRegister.Rdi], requireOwner: true);
private static int PthreadGetthreadidCore(CpuContext ctx)
{
ctx[CpuRegister.Rax] = KernelPthreadState.GetCurrentThreadUniqueId();
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "F8bUHwAG284",
ExportName = "scePthreadMutexattrInit",
@@ -232,6 +238,13 @@ public static class KernelPthreadCompatExports
LibraryName = "libKernel")]
public static int PthreadCondInit(CpuContext ctx) => PthreadCondInitCore(ctx, ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "0TyVk4MSLt0",
ExportName = "pthread_cond_init",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondInit(CpuContext ctx) => PthreadCondInitCore(ctx, ctx[CpuRegister.Rdi]);
[SysAbiExport(
Nid = "g+PZd2hiacg",
ExportName = "scePthreadCondDestroy",
@@ -258,14 +271,14 @@ public static class KernelPthreadCompatExports
ExportName = "scePthreadCondSignal",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: false);
public static int PthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
[SysAbiExport(
Nid = "JGgj7Uvrl+A",
ExportName = "scePthreadCondBroadcast",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: true);
public static int PthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: true);
[SysAbiExport(
Nid = "Op8TBGY5KHg",
@@ -279,7 +292,14 @@ public static class KernelPthreadCompatExports
ExportName = "pthread_cond_broadcast",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx[CpuRegister.Rdi], broadcast: true);
public static int PosixPthreadCondBroadcast(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: true);
[SysAbiExport(
Nid = "2MOy+rUfuhQ",
ExportName = "pthread_cond_signal",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
[SysAbiExport(
Nid = "m5-2bsNfv7s",
@@ -402,6 +422,23 @@ public static class KernelPthreadCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp)
{
if (tryOnly)
{
TracePthreadMutex(ctx, "trylock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
// Normal/adaptive mutexes do not report EDEADLK on self-lock.
// Under the current single-host-thread guest execution model,
// treating them as nested ownership keeps init paths moving
// without turning a would-block path into a hard error.
state.RecursionCount++;
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
var ownedResult = tryOnly
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
@@ -606,7 +643,7 @@ public static class KernelPthreadCompatExports
return mutexAddress;
}
private static bool TryResolveMutexState(CpuContext ctx, ulong mutexAddress, bool createIfZero, out ulong resolvedAddress, out PthreadMutexState? state)
private static bool TryResolveMutexState(CpuContext ctx, ulong mutexAddress, bool createIfZero, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
{
resolvedAddress = 0;
state = null;
@@ -629,6 +666,11 @@ public static class KernelPthreadCompatExports
return false;
}
if (pointedHandle == StaticAdaptiveMutexInitializer)
{
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeAdaptiveNp, out resolvedAddress, out state);
}
if (pointedHandle != 0)
{
lock (_stateGate)
@@ -651,18 +693,7 @@ public static class KernelPthreadCompatExports
return false;
}
var createdState = new PthreadMutexState();
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexHandleBase, ref _nextSyntheticMutexHandleId);
lock (_stateGate)
{
_mutexStates[mutexAddress] = createdState;
_mutexStates[syntheticHandle] = createdState;
}
_ = ctx.TryWriteUInt64(mutexAddress, syntheticHandle);
resolvedAddress = syntheticHandle;
state = createdState;
return true;
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeDefault, out resolvedAddress, out state);
}
private static ulong ResolveMutexAttrHandle(CpuContext ctx, ulong attrAddress)
@@ -739,7 +770,7 @@ public static class KernelPthreadCompatExports
return condAddress;
}
private static bool TryResolveCondState(CpuContext? ctx, ulong condAddress, bool createIfZero, out ulong resolvedAddress, out PthreadCondState? state)
private static bool TryResolveCondState(CpuContext? ctx, ulong condAddress, bool createIfZero, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadCondState? state)
{
resolvedAddress = 0;
state = null;
@@ -857,6 +888,7 @@ public static class KernelPthreadCompatExports
}
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
var spuriousWake = false;
lock (state.SyncRoot)
{
state.Waiters++;
@@ -871,11 +903,36 @@ public static class KernelPthreadCompatExports
return unlockResult;
}
var scheduler = GuestThreadExecution.Scheduler;
if (!timed && GuestThreadExecution.RequestCurrentThreadBlock("pthread_cond_wait"))
{
TracePthreadCond("wait-block", condAddress, mutexAddress, state, timed, waitResult);
return waitResult;
}
if (scheduler is not null)
{
Monitor.Exit(state.SyncRoot);
try
{
scheduler.Pump(ctx, "pthread_cond_wait");
}
finally
{
Monitor.Enter(state.SyncRoot);
}
}
while (state.SignalEpoch == observedEpoch)
{
if (!timed)
{
Monitor.Wait(state.SyncRoot);
if (!Monitor.Wait(state.SyncRoot, GetCondSpuriousWakeTimeout()))
{
spuriousWake = true;
break;
}
continue;
}
@@ -887,7 +944,15 @@ public static class KernelPthreadCompatExports
}
state.Waiters = Math.Max(0, state.Waiters - 1);
TracePthreadCond(waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-wake" : "wait-timeout", condAddress, mutexAddress, state, timed, waitResult);
TracePthreadCond(
spuriousWake
? "wait-spurious"
: (waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-wake" : "wait-timeout"),
condAddress,
mutexAddress,
state,
timed,
waitResult);
}
var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
@@ -897,18 +962,26 @@ public static class KernelPthreadCompatExports
return lockResult;
}
TracePthreadCond(waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-exit" : "wait-exit-timeout", condAddress, mutexAddress, state, timed, waitResult);
TracePthreadCond(
spuriousWake
? "wait-exit-spurious"
: (waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-exit" : "wait-exit-timeout"),
condAddress,
mutexAddress,
state,
timed,
waitResult);
return waitResult;
}
private static int PthreadCondSignalCore(ulong condAddress, bool broadcast)
private static int PthreadCondSignalCore(CpuContext ctx, ulong condAddress, bool broadcast)
{
if (condAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveCondState(null, condAddress, createIfZero: false, out _, out var state))
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
@@ -944,6 +1017,16 @@ public static class KernelPthreadCompatExports
return TimeSpan.FromTicks((long)timeoutUsec * 10L);
}
private static TimeSpan GetCondSpuriousWakeTimeout()
{
if (int.TryParse(Environment.GetEnvironmentVariable("SHARPEMU_PTHREAD_COND_SPURIOUS_WAKE_MS"), out var milliseconds))
{
return TimeSpan.FromMilliseconds(Math.Max(1, milliseconds));
}
return TimeSpan.FromMilliseconds(DefaultSpuriousCondWakeMilliseconds);
}
private static int NormalizeMutexType(int type)
{
return type switch
@@ -952,11 +1035,43 @@ public static class KernelPthreadCompatExports
1 => MutexTypeErrorCheck,
2 => MutexTypeRecursive,
3 => MutexTypeNormal,
4 => MutexTypeNormal,
4 => MutexTypeAdaptiveNp,
_ => MutexTypeDefault,
};
}
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
{
var createdState = new PthreadMutexState
{
Type = type,
};
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexHandleBase, ref _nextSyntheticMutexHandleId);
lock (_stateGate)
{
if (_mutexStates.TryGetValue(mutexAddress, out state))
{
resolvedAddress = mutexAddress;
return true;
}
if (_mutexStates.TryGetValue(syntheticHandle, out state))
{
resolvedAddress = syntheticHandle;
return true;
}
_mutexStates[mutexAddress] = createdState;
_mutexStates[syntheticHandle] = createdState;
}
_ = ctx.TryWriteUInt64(mutexAddress, syntheticHandle);
resolvedAddress = syntheticHandle;
state = createdState;
return true;
}
private static void TracePthreadSelf(CpuContext ctx, ulong currentThreadHandle)
{
if (!ShouldTracePthread())
@@ -5,6 +5,7 @@ using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
@@ -19,17 +20,18 @@ public static class KernelPthreadExtendedCompatExports
private const int DefaultSchedPolicy = 0;
private const int DefaultSchedPriority = 0;
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_0000_0000;
private static readonly object _stateGate = new();
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
private static readonly Dictionary<ulong, PthreadAttrState> _attrStates = new();
private static readonly Dictionary<ulong, ReaderWriterLockSlim> _rwlockStates = new();
private static readonly Dictionary<ulong, PthreadRwlockState> _rwlockStates = new();
private static readonly Dictionary<int, TlsKeyState> _tlsKeys = new();
private static int _nextTlsKey = 1;
private static long _nextSyntheticRwlockHandleId = 1;
private static long _nextSyntheticPthreadAttrHandleId = 1;
[ThreadStatic]
private static Dictionary<int, ulong>? _threadLocalSpecific;
private static readonly Dictionary<ulong, Dictionary<int, ulong>> _threadLocalSpecific = new();
private sealed class ThreadState
{
@@ -40,6 +42,47 @@ public static class KernelPthreadExtendedCompatExports
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
}
private sealed class PthreadRwlockState
{
public object SyncRoot { get; } = new();
public Dictionary<ulong, int> ReaderCounts { get; } = new();
public int ReaderTotalCount { get; set; }
public ulong WriterThreadId { get; set; }
public int WaitingWriters { get; set; }
public int GetReaderCount(ulong threadId)
{
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
}
public void AddReader(ulong threadId)
{
ReaderCounts.TryGetValue(threadId, out var currentCount);
ReaderCounts[threadId] = currentCount + 1;
ReaderTotalCount++;
}
public bool RemoveReader(ulong threadId)
{
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
{
return false;
}
if (currentCount == 1)
{
ReaderCounts.Remove(threadId);
}
else
{
ReaderCounts[threadId] = currentCount - 1;
}
ReaderTotalCount = Math.Max(0, ReaderTotalCount - 1);
return true;
}
}
private readonly record struct TlsKeyState(ulong Destructor);
private readonly record struct PthreadAttrState(
@@ -194,6 +237,41 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Xs9hdiD7sAA",
ExportName = "pthread_setschedparam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadSetschedparam(CpuContext ctx)
{
var thread = ctx[CpuRegister.Rdi];
var policy = unchecked((int)ctx[CpuRegister.Rsi]);
var schedParamAddress = ctx[CpuRegister.Rdx];
if (thread == 0 || schedParamAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
lock (_stateGate)
{
var state = GetOrCreateThreadStateLocked(thread);
state.Priority = schedPriority;
state.Attributes = state.Attributes with
{
SchedPolicy = policy,
SchedPriority = schedPriority,
};
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "nsYoNRywwNg",
ExportName = "scePthreadAttrInit",
@@ -207,15 +285,32 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var syntheticHandle = AllocateSyntheticHandle(SyntheticPthreadAttrHandleBase, ref _nextSyntheticPthreadAttrHandleId);
if (!ctx.TryWriteUInt64(attrAddress, syntheticHandle))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
lock (_stateGate)
{
_attrStates[attrAddress] = PthreadAttrState.Default;
_attrStates[syntheticHandle] = PthreadAttrState.Default;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "wtkt-teR1so",
ExportName = "pthread_attr_init",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadAttrInit(CpuContext ctx)
{
return PthreadAttrInit(ctx);
}
[SysAbiExport(
Nid = "62KCwEMmzcM",
ExportName = "scePthreadAttrDestroy",
@@ -229,15 +324,31 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
lock (_stateGate)
{
_attrStates.Remove(attrAddress);
if (resolvedAddress != attrAddress)
{
_attrStates.Remove(resolvedAddress);
}
}
_ = ctx.TryWriteUInt64(attrAddress, 0);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "zHchY8ft5pk",
ExportName = "pthread_attr_destroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadAttrDestroy(CpuContext ctx)
{
return PthreadAttrDestroy(ctx);
}
[SysAbiExport(
Nid = "x1X76arYMxU",
ExportName = "scePthreadAttrGet",
@@ -570,16 +681,32 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
lock (_stateGate)
{
var state = GetOrCreateAttrStateLocked(attrAddress);
_attrStates[attrAddress] = state with { StackSize = stackSize };
var state = GetOrCreateAttrStateLocked(resolvedAddress);
var updated = state with { StackSize = stackSize };
_attrStates[resolvedAddress] = updated;
if (resolvedAddress != attrAddress)
{
_attrStates[attrAddress] = updated;
}
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "2Q0z6rnBrTE",
ExportName = "pthread_attr_setstacksize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int PosixPthreadAttrSetstacksize(CpuContext ctx)
{
return PthreadAttrSetstacksize(ctx);
}
[SysAbiExport(
Nid = "6ULAa0fq4jA",
ExportName = "scePthreadRwlockInit",
@@ -599,10 +726,9 @@ public static class KernelPthreadExtendedCompatExports
var resolvedAddress = ResolveRwlockHandle(ctx, rwlockAddress);
if (_rwlockStates.Remove(resolvedAddress, out var existing))
{
existing.Dispose();
}
var rwlock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
var rwlock = new PthreadRwlockState();
_rwlockStates[rwlockAddress] = rwlock;
_rwlockStates[syntheticHandle] = rwlock;
}
@@ -633,14 +759,10 @@ public static class KernelPthreadExtendedCompatExports
}
var resolvedAddress = ResolveRwlockHandle(ctx, rwlockAddress);
ReaderWriterLockSlim? state;
PthreadRwlockState? state;
lock (_stateGate)
{
_rwlockStates.Remove(resolvedAddress, out state);
if (resolvedAddress != rwlockAddress)
{
_rwlockStates.Remove(rwlockAddress);
}
_rwlockStates.TryGetValue(resolvedAddress, out state);
}
if (state is null)
@@ -648,8 +770,24 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
lock (state.SyncRoot)
{
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
}
}
lock (_stateGate)
{
_rwlockStates.Remove(resolvedAddress);
if (resolvedAddress != rwlockAddress)
{
_rwlockStates.Remove(rwlockAddress);
}
}
_ = ctx.TryWriteUInt64(rwlockAddress, 0);
state.Dispose();
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -702,29 +840,38 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out _, out var rwlock))
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out _, out var rwlock))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
try
{
if (rwlock.IsWriteLockHeld)
lock (rwlock.SyncRoot)
{
rwlock.ExitWriteLock();
}
else if (rwlock.IsReadLockHeld)
{
rwlock.ExitReadLock();
}
else
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
if (rwlock.WriterThreadId == currentThreadId)
{
rwlock.WriterThreadId = 0;
Monitor.PulseAll(rwlock.SyncRoot);
}
else if (rwlock.RemoveReader(currentThreadId))
{
if (rwlock.ReaderTotalCount == 0 || rwlock.WaitingWriters > 0)
{
Monitor.PulseAll(rwlock.SyncRoot);
}
}
else
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
}
}
catch (SynchronizationLockException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
@@ -793,9 +940,13 @@ public static class KernelPthreadExtendedCompatExports
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
foreach (var entry in _threadLocalSpecific)
{
entry.Value.Remove(key);
}
}
_threadLocalSpecific?.Remove(key);
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -816,16 +967,23 @@ public static class KernelPthreadExtendedCompatExports
{
var key = unchecked((int)ctx[CpuRegister.Rdi]);
var value = ctx[CpuRegister.Rsi];
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
lock (_stateGate)
{
if (!_tlsKeys.TryGetValue(key, out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
if (!_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values))
{
values = new Dictionary<int, ulong>();
_threadLocalSpecific[currentThreadHandle] = values;
}
values[key] = value;
}
_threadLocalSpecific ??= new Dictionary<int, ulong>();
_threadLocalSpecific[key] = value;
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -845,6 +1003,8 @@ public static class KernelPthreadExtendedCompatExports
public static int PosixPthreadGetspecific(CpuContext ctx)
{
var key = unchecked((int)ctx[CpuRegister.Rdi]);
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
ulong value = 0;
lock (_stateGate)
{
if (!_tlsKeys.TryGetValue(key, out _))
@@ -852,12 +1012,15 @@ public static class KernelPthreadExtendedCompatExports
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) &&
values.TryGetValue(key, out var storedValue))
{
value = storedValue;
}
}
ctx[CpuRegister.Rax] =
_threadLocalSpecific is not null && _threadLocalSpecific.TryGetValue(key, out var value)
? value
: 0UL;
ctx[CpuRegister.Rax] = value;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -880,21 +1043,47 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
try
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
lock (rwlock.SyncRoot)
{
if (write)
{
rwlock.EnterWriteLock();
if (rwlock.WriterThreadId == currentThreadId || rwlock.GetReaderCount(currentThreadId) > 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
rwlock.WaitingWriters++;
try
{
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0)
{
Monitor.Wait(rwlock.SyncRoot);
}
}
finally
{
rwlock.WaitingWriters--;
}
rwlock.WriterThreadId = currentThreadId;
}
else
{
rwlock.EnterReadLock();
if (rwlock.WriterThreadId == currentThreadId)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
}
while (rwlock.WriterThreadId != 0 ||
(rwlock.WaitingWriters > 0 && rwlock.GetReaderCount(currentThreadId) == 0))
{
Monitor.Wait(rwlock.SyncRoot);
}
rwlock.AddReader(currentThreadId);
}
}
catch (LockRecursionException)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -928,7 +1117,7 @@ public static class KernelPthreadExtendedCompatExports
return rwlockAddress;
}
private static bool TryResolveRwlockState(CpuContext ctx, ulong rwlockAddress, bool createIfZero, out ulong resolvedAddress, out ReaderWriterLockSlim? rwlock)
private static bool TryResolveRwlockState(CpuContext ctx, ulong rwlockAddress, bool createIfZero, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadRwlockState? rwlock)
{
resolvedAddress = 0;
rwlock = null;
@@ -973,7 +1162,7 @@ public static class KernelPthreadExtendedCompatExports
return false;
}
var createdRwlock = new ReaderWriterLockSlim(LockRecursionPolicy.SupportsRecursion);
var createdRwlock = new PthreadRwlockState();
var syntheticHandle = AllocateSyntheticHandle(SyntheticRwlockHandleBase, ref _nextSyntheticRwlockHandleId);
lock (_stateGate)
{
@@ -1028,6 +1217,35 @@ public static class KernelPthreadExtendedCompatExports
return state;
}
private static ulong ResolvePthreadAttrHandle(CpuContext ctx, ulong attrAddress)
{
if (attrAddress == 0)
{
return 0;
}
lock (_stateGate)
{
if (_attrStates.ContainsKey(attrAddress))
{
return attrAddress;
}
}
if (ctx.TryReadUInt64(attrAddress, out var pointedHandle) && pointedHandle != 0)
{
lock (_stateGate)
{
if (_attrStates.ContainsKey(pointedHandle))
{
return pointedHandle;
}
}
}
return attrAddress;
}
private static bool TryWriteFixedUtf8CString(CpuContext ctx, ulong address, string value, int maxBytes)
{
if (maxBytes <= 0)
@@ -3,6 +3,7 @@
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Kernel;
@@ -25,12 +26,24 @@ internal static class KernelPthreadState
internal static ulong GetCurrentThreadHandle()
{
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out _))
{
return guestThreadHandle;
}
EnsureCurrentThreadRegistered();
return _currentThreadHandle;
}
internal static ulong GetCurrentThreadUniqueId()
{
var guestThreadHandle = GuestThreadExecution.CurrentGuestThreadHandle;
if (guestThreadHandle != 0 && TryGetThreadIdentity(guestThreadHandle, out var identity))
{
return identity.UniqueId;
}
EnsureCurrentThreadRegistered();
return _currentThreadUniqueId;
}
@@ -31,6 +31,7 @@ public static class KernelRuntimeCompatExports
private const ulong DefaultKernelTscFrequency = 10_000_000UL;
private const ulong PrtAreaStartAddress = 0x0000001000000000UL;
private const ulong PrtAreaSize = 0x000000EC00000000UL;
private const int AioInitParamSize = 0x3C;
private const uint MemCommit = 0x1000;
private const uint MemReserve = 0x2000;
private const uint PageExecuteReadWrite = 0x40;
@@ -257,6 +258,8 @@ public static class KernelRuntimeCompatExports
}
}
TraceProcParam(ctx, address);
ctx[CpuRegister.Rax] = address;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -269,6 +272,223 @@ public static class KernelRuntimeCompatExports
}
}
private static void TraceProcParam(CpuContext ctx, ulong address)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PROC_PARAM"), "1", StringComparison.Ordinal))
{
return;
}
if (address == 0)
{
Console.Error.WriteLine("[LOADER][TRACE] proc_param: address=0");
return;
}
const int dumpSize = 0x200;
var buffer = GC.AllocateUninitializedArray<byte>(dumpSize);
if (!ctx.Memory.TryRead(address, buffer))
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param: address=0x{address:X16} unreadable");
return;
}
Console.Error.WriteLine($"[LOADER][TRACE] proc_param: address=0x{address:X16} size=0x{dumpSize:X}");
for (var offset = 0; offset < dumpSize; offset += 16)
{
var slice = buffer.AsSpan(offset, 16);
var hex = Convert.ToHexString(slice);
Console.Error.WriteLine($"[LOADER][TRACE] proc_param[{offset:X3}]: {hex}");
}
TraceProcParamPointers(ctx, address, buffer);
}
private static void TraceProcParamPointers(CpuContext ctx, ulong baseAddress, ReadOnlySpan<byte> buffer)
{
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PROC_PARAM_PTRS"), "1", StringComparison.Ordinal))
{
return;
}
if (buffer.Length < 0x50)
{
return;
}
for (var offset = 0x20; offset <= 0x48; offset += 8)
{
var ptr = BinaryPrimitives.ReadUInt64LittleEndian(buffer.Slice(offset, 8));
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr@{offset:X2}: 0x{ptr:X16}");
if (ptr == 0)
{
continue;
}
TraceProcParamPointerTarget(ctx, ptr);
}
}
private static void TraceProcParamPointerTarget(CpuContext ctx, ulong address)
{
const int maxAsciiBytes = 256;
const int maxWideChars = 128;
if (TryReadUtf8CString(ctx, address, maxAsciiBytes, out var asciiValue))
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target ascii@0x{address:X16}: \"{asciiValue}\"");
return;
}
if (TryReadUtf16CString(ctx, address, maxWideChars, out var wideValue))
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target wide@0x{address:X16}: \"{wideValue}\"");
return;
}
var preview = GC.AllocateUninitializedArray<byte>(64);
if (ctx.Memory.TryRead(address, preview))
{
var hex = Convert.ToHexString(preview);
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target hex@0x{address:X16}: {hex}");
TraceProcParamEmbeddedPointers(ctx, address, preview);
}
else
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.target unreadable@0x{address:X16}");
}
}
private static bool TryReadUtf8CString(CpuContext ctx, ulong address, int maxBytes, out string value)
{
value = string.Empty;
var buffer = GC.AllocateUninitializedArray<byte>(maxBytes);
if (!ctx.Memory.TryRead(address, buffer))
{
return false;
}
var length = Array.IndexOf(buffer, (byte)0);
if (length < 0)
{
length = maxBytes;
}
if (length == 0)
{
return false;
}
var text = Encoding.UTF8.GetString(buffer, 0, length);
if (!IsMostlyPrintable(text))
{
return false;
}
value = text;
return true;
}
private static bool TryReadUtf16CString(CpuContext ctx, ulong address, int maxChars, out string value)
{
value = string.Empty;
var maxBytes = maxChars * 2;
var buffer = GC.AllocateUninitializedArray<byte>(maxBytes);
if (!ctx.Memory.TryRead(address, buffer))
{
return false;
}
var lengthBytes = -1;
for (var i = 0; i + 1 < buffer.Length; i += 2)
{
if (buffer[i] == 0 && buffer[i + 1] == 0)
{
lengthBytes = i;
break;
}
}
if (lengthBytes <= 0)
{
return false;
}
var text = Encoding.Unicode.GetString(buffer, 0, lengthBytes);
if (!IsMostlyPrintable(text))
{
return false;
}
value = text;
return true;
}
private static bool IsMostlyPrintable(string text)
{
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
var printable = 0;
for (var i = 0; i < text.Length; i++)
{
var ch = text[i];
if (ch == '\0')
{
continue;
}
if (!char.IsControl(ch) || ch == '\r' || ch == '\n' || ch == '\t')
{
printable++;
}
}
return printable >= Math.Max(4, text.Length * 3 / 4);
}
private static void TraceProcParamEmbeddedPointers(CpuContext ctx, ulong baseAddress, ReadOnlySpan<byte> data)
{
const int maxCandidates = 12;
var found = 0;
Span<byte> probe = stackalloc byte[2];
for (var offset = 0; offset + 8 <= data.Length; offset += 8)
{
var candidate = BinaryPrimitives.ReadUInt64LittleEndian(data.Slice(offset, 8));
if (candidate == 0)
{
continue;
}
if (!ctx.Memory.TryRead(candidate, probe))
{
continue;
}
if (TryReadUtf8CString(ctx, candidate, 256, out var ascii))
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.embed@0x{baseAddress:X16}+0x{offset:X2} -> 0x{candidate:X16} ascii \"{ascii}\"");
if (++found >= maxCandidates)
{
return;
}
continue;
}
if (TryReadUtf16CString(ctx, candidate, 128, out var wide))
{
Console.Error.WriteLine($"[LOADER][TRACE] proc_param.ptr.embed@0x{baseAddress:X16}+0x{offset:X2} -> 0x{candidate:X16} wide \"{wide}\"");
if (++found >= maxCandidates)
{
return;
}
}
}
}
[SysAbiExport(
Nid = "9BcDykPmo1I",
ExportName = "__error",
@@ -780,6 +1000,48 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "nu4a0-arQis",
ExportName = "sceKernelAioInitializeParam",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAioInitializeParam(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Span<byte> zero = stackalloc byte[AioInitParamSize];
zero.Clear();
if (!ctx.Memory.TryWrite(paramAddress, zero))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "vYU8P9Td2Zo",
ExportName = "sceKernelAioInitializeImpl",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libKernel")]
public static int KernelAioInitializeImpl(CpuContext ctx)
{
var paramAddress = ctx[CpuRegister.Rdi];
var size = unchecked((int)ctx[CpuRegister.Rsi]);
if (paramAddress == 0 || size < AioInitParamSize)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "eR2bZFAAU0Q",
ExportName = "sceSysmoduleUnloadModule",
+176
View File
@@ -0,0 +1,176 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
namespace SharpEmu.Libs.PlayGo;
public static class PlayGoExports
{
private const int OrbisPlayGoErrorInvalidArgument = unchecked((int)0x80B20004);
private const int OrbisPlayGoErrorNotInitialized = unchecked((int)0x80B20005);
private const int OrbisPlayGoErrorAlreadyInitialized = unchecked((int)0x80B20006);
private const int OrbisPlayGoErrorBadHandle = unchecked((int)0x80B20009);
private const int OrbisPlayGoErrorBadPointer = unchecked((int)0x80B2000A);
private const int OrbisPlayGoErrorBadSize = unchecked((int)0x80B2000B);
private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E);
private const ulong PlayGoInitBufAddrOffset = 0;
private const ulong PlayGoInitBufSizeOffset = 8;
private const uint PlayGoMinimumInitBufferSize = 0x200000;
private const uint PlayGoHandle = 1;
private static readonly object _stateGate = new();
private static bool _initialized;
private static bool _hasPlayGoData;
[SysAbiExport(
Nid = "ts6GlZOKRrE",
ExportName = "scePlayGoInitialize",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoInitialize(CpuContext ctx)
{
var initParamsAddress = ctx[CpuRegister.Rdi];
if (initParamsAddress == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (!ctx.TryReadUInt64(initParamsAddress + PlayGoInitBufAddrOffset, out var bufferAddress))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
Span<byte> bufferSizeBytes = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(initParamsAddress + PlayGoInitBufSizeOffset, bufferSizeBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
var bufferSize = BinaryPrimitives.ReadUInt32LittleEndian(bufferSizeBytes);
if (bufferAddress == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (bufferSize < PlayGoMinimumInitBufferSize)
{
return OrbisPlayGoErrorBadSize;
}
lock (_stateGate)
{
if (_initialized)
{
return OrbisPlayGoErrorAlreadyInitialized;
}
_hasPlayGoData = HasPlayGoChunkData();
_initialized = true;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "M1Gma1ocrGE",
ExportName = "scePlayGoOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoOpen(CpuContext ctx)
{
var outHandleAddress = ctx[CpuRegister.Rdi];
var paramAddress = ctx[CpuRegister.Rsi];
if (outHandleAddress == 0)
{
return OrbisPlayGoErrorBadPointer;
}
if (paramAddress != 0)
{
return OrbisPlayGoErrorInvalidArgument;
}
lock (_stateGate)
{
if (!_initialized)
{
return OrbisPlayGoErrorNotInitialized;
}
if (!_hasPlayGoData)
{
return OrbisPlayGoErrorNotSupportPlayGo;
}
}
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(handleBytes, PlayGoHandle);
if (!ctx.Memory.TryWrite(outHandleAddress, handleBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MPe0EeBGM-E",
ExportName = "scePlayGoTerminate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoTerminate(CpuContext ctx)
{
_ = ctx;
lock (_stateGate)
{
if (!_initialized)
{
return OrbisPlayGoErrorNotInitialized;
}
_initialized = false;
_hasPlayGoData = false;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "Uco1I0dlDi8",
ExportName = "scePlayGoClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePlayGo")]
public static int PlayGoClose(CpuContext ctx)
{
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
lock (_stateGate)
{
if (!_initialized)
{
return OrbisPlayGoErrorNotInitialized;
}
if (handle != PlayGoHandle)
{
return OrbisPlayGoErrorBadHandle;
}
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool HasPlayGoChunkData()
{
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
if (string.IsNullOrWhiteSpace(app0Root))
{
return false;
}
var hostPath = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
return File.Exists(hostPath);
}
}
@@ -2,9 +2,408 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
namespace SharpEmu.Libs.VideoOut;
public static class VideoOutExports
{
private const int OrbisVideoOutErrorInvalidValue = unchecked((int)0x80290001);
private const int OrbisVideoOutErrorInvalidAddress = unchecked((int)0x80290002);
private const int OrbisVideoOutErrorResourceBusy = unchecked((int)0x80290009);
private const int OrbisVideoOutErrorInvalidIndex = unchecked((int)0x8029000A);
private const int OrbisVideoOutErrorInvalidHandle = unchecked((int)0x8029000B);
private const int OrbisVideoOutErrorInvalidOption = unchecked((int)0x8029001A);
private const int SceVideoOutBusTypeMain = 0;
private const int SceVideoOutBufferAttributeOptionNone = 0;
private const int MaxOpenPorts = 4;
private const int MaxDisplayBuffers = 16;
private const int VideoOutBufferAttributeSize = 0x24;
private const int VideoOutBufferAttribute2Size = 0x50;
private const int VideoOutBuffersEntrySize = 0x20;
private static readonly object _stateGate = new();
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
private static int _nextHandle = 1;
private sealed class VideoOutPortState
{
public required int Handle { get; init; }
public int FlipRate { get; set; }
public ulong VblankCount { get; set; }
public int NextSetId { get; set; } = 1;
public HashSet<int> RegisteredSetIds { get; } = new();
}
[SysAbiExport(
Nid = "Up36PTk687E",
ExportName = "sceVideoOutOpen",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutOpen(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var busType = unchecked((int)ctx[CpuRegister.Rsi]);
var index = unchecked((int)ctx[CpuRegister.Rdx]);
_ = ctx[CpuRegister.Rcx];
if (busType != SceVideoOutBusTypeMain || index != 0)
{
return OrbisVideoOutErrorInvalidValue;
}
if (userId != 0 && userId != 255)
{
return OrbisVideoOutErrorInvalidValue;
}
lock (_stateGate)
{
if (_ports.Count >= MaxOpenPorts)
{
return OrbisVideoOutErrorResourceBusy;
}
var handle = _nextHandle++;
_ports[handle] = new VideoOutPortState
{
Handle = handle,
};
return handle;
}
}
[SysAbiExport(
Nid = "uquVH4-Du78",
ExportName = "sceVideoOutClose",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutClose(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
lock (_stateGate)
{
_ports.Remove(handle);
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "CBiu4mCE1DA",
ExportName = "sceVideoOutSetFlipRate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutSetFlipRate(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var rate = unchecked((int)ctx[CpuRegister.Rsi]);
if (rate is < 0 or > 2)
{
return OrbisVideoOutErrorInvalidValue;
}
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
port.FlipRate = rate;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "j6RaAUlaLv0",
ExportName = "sceVideoOutWaitVblank",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutWaitVblank(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
Thread.Sleep(1);
lock (_stateGate)
{
port.VblankCount++;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "MTxxrOCeSig",
ExportName = "sceVideoOutSetWindowModeMargins",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutSetWindowModeMargins(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
_ = unchecked((int)ctx[CpuRegister.Rsi]);
_ = unchecked((int)ctx[CpuRegister.Rdx]);
return TryGetPort(handle, out _)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: OrbisVideoOutErrorInvalidHandle;
}
[SysAbiExport(
Nid = "N5KDtkIjjJ4",
ExportName = "sceVideoOutUnregisterBuffers",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutUnregisterBuffers(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var attributeIndex = unchecked((int)ctx[CpuRegister.Rsi]);
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
if (attributeIndex < 0)
{
return OrbisVideoOutErrorInvalidValue;
}
lock (_stateGate)
{
return port.RegisteredSetIds.Remove(attributeIndex)
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: OrbisVideoOutErrorInvalidValue;
}
}
[SysAbiExport(
Nid = "i6-sR91Wt-4",
ExportName = "sceVideoOutSetBufferAttribute",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutSetBufferAttribute(CpuContext ctx)
{
var attributeAddress = ctx[CpuRegister.Rdi];
var pixelFormat = unchecked((uint)ctx[CpuRegister.Rsi]);
var tilingMode = unchecked((uint)ctx[CpuRegister.Rdx]);
var aspectRatio = unchecked((uint)ctx[CpuRegister.Rcx]);
var width = unchecked((uint)ctx[CpuRegister.R8]);
var height = unchecked((uint)ctx[CpuRegister.R9]);
if (!TryReadStackUInt32(ctx, 0, out var pitchInPixel))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (attributeAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
Span<byte> attribute = stackalloc byte[VideoOutBufferAttributeSize];
attribute.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x00..0x04], pixelFormat);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x04..0x08], tilingMode);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x08..0x0C], aspectRatio);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x0C..0x10], width);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x10..0x14], height);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x14..0x18], pitchInPixel);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x18..0x1C], SceVideoOutBufferAttributeOptionNone);
if (!ctx.Memory.TryWrite(attributeAddress, attribute))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "PjS5uASwcV8",
ExportName = "sceVideoOutSetBufferAttribute2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutSetBufferAttribute2(CpuContext ctx)
{
var attributeAddress = ctx[CpuRegister.Rdi];
var pixelFormat = ctx[CpuRegister.Rsi];
var tilingMode = unchecked((uint)ctx[CpuRegister.Rdx]);
var width = unchecked((uint)ctx[CpuRegister.Rcx]);
var height = unchecked((uint)ctx[CpuRegister.R8]);
var option = ctx[CpuRegister.R9];
if (!TryReadStackUInt32(ctx, 0, out var dccControl) ||
!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + 0x10, out var dccClearColor))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (attributeAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
Span<byte> attribute = stackalloc byte[VideoOutBufferAttribute2Size];
attribute.Clear();
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x04..0x08], tilingMode);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x0C..0x10], width);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x10..0x14], height);
BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x18..0x20], option);
BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x20..0x28], pixelFormat);
BinaryPrimitives.WriteUInt64LittleEndian(attribute[0x28..0x30], dccClearColor);
BinaryPrimitives.WriteUInt32LittleEndian(attribute[0x30..0x34], dccControl);
if (!ctx.Memory.TryWrite(attributeAddress, attribute))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
[SysAbiExport(
Nid = "w3BY+tAEiQY",
ExportName = "sceVideoOutRegisterBuffers",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutRegisterBuffers(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var startIndex = unchecked((int)ctx[CpuRegister.Rsi]);
var addressesAddress = ctx[CpuRegister.Rdx];
var bufferNum = unchecked((int)ctx[CpuRegister.Rcx]);
var attributeAddress = ctx[CpuRegister.R8];
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
if (addressesAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
if (attributeAddress == 0)
{
return OrbisVideoOutErrorInvalidOption;
}
if (!IsValidBufferRange(startIndex, bufferNum))
{
return OrbisVideoOutErrorInvalidValue;
}
for (var i = 0; i < bufferNum; i++)
{
if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
var setId = AllocateBufferSet(port);
return setId;
}
[SysAbiExport(
Nid = "rKBUtgRrtbk",
ExportName = "sceVideoOutRegisterBuffers2",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceVideoOut")]
public static int VideoOutRegisterBuffers2(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var setIndex = unchecked((int)ctx[CpuRegister.Rsi]);
var bufferIndexStart = unchecked((int)ctx[CpuRegister.Rdx]);
var buffersAddress = ctx[CpuRegister.Rcx];
var bufferNum = unchecked((int)ctx[CpuRegister.R8]);
var attributeAddress = ctx[CpuRegister.R9];
if (!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + 0x08, out var categoryRaw) ||
!ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + 0x10, out var option))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!TryGetPort(handle, out var port))
{
return OrbisVideoOutErrorInvalidHandle;
}
if (buffersAddress == 0)
{
return OrbisVideoOutErrorInvalidAddress;
}
if (attributeAddress == 0)
{
return OrbisVideoOutErrorInvalidOption;
}
if (!IsValidBufferRange(bufferIndexStart, bufferNum))
{
return OrbisVideoOutErrorInvalidValue;
}
if (categoryRaw != 0 || option != 0)
{
return OrbisVideoOutErrorInvalidValue;
}
for (var i = 0; i < bufferNum; i++)
{
var entryAddress = buffersAddress + ((ulong)i * VideoOutBuffersEntrySize);
if (!ctx.TryReadUInt64(entryAddress + 0x00, out _) ||
!ctx.TryReadUInt64(entryAddress + 0x08, out _))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
lock (_stateGate)
{
port.RegisteredSetIds.Add(setIndex);
}
return setIndex;
}
private static int AllocateBufferSet(VideoOutPortState port)
{
lock (_stateGate)
{
var setId = port.NextSetId++;
port.RegisteredSetIds.Add(setId);
return setId;
}
}
private static bool IsValidBufferRange(int startIndex, int bufferNum)
{
return startIndex >= 0 &&
startIndex < MaxDisplayBuffers &&
bufferNum >= 1 &&
bufferNum <= MaxDisplayBuffers &&
startIndex + bufferNum <= MaxDisplayBuffers;
}
private static bool TryGetPort(int handle, [NotNullWhen(true)] out VideoOutPortState? port)
{
lock (_stateGate)
{
return _ports.TryGetValue(handle, out port);
}
}
private static bool TryReadStackUInt32(CpuContext ctx, int stackIndex, out uint value)
{
var address = ctx[CpuRegister.Rsp] + 0x08 + ((ulong)stackIndex * 0x08);
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
}