mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
12 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7bd1e8dc29 | |||
| fad61e116e | |||
| bc297fa175 | |||
| 79e767ed9f | |||
| 1ea1396979 | |||
| a1dea499bc | |||
| fee6af3136 | |||
| bbf5ff7be8 | |||
| 0c859f04ad | |||
| 994438963d | |||
| 233af123af | |||
| 02eb9b30e9 |
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
@@ -29,6 +29,8 @@ arm64/
|
||||
packages/
|
||||
*.nupkg
|
||||
.nuget/
|
||||
.dotnet-home/
|
||||
.cache/
|
||||
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@ path = [
|
||||
"**/packages.lock.json",
|
||||
"scripts/ps5_names.txt",
|
||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||
"_logs/**"
|
||||
"_logs/**",
|
||||
".github/images/**"
|
||||
]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.103",
|
||||
"rollForward": "latestFeature"
|
||||
"rollForward": "disable"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,10 +30,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<KnownILLinkPack Update="Microsoft.NET.ILLink.Tasks" ILLinkPackVersion="10.0.5" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
|
||||
@@ -4,9 +4,9 @@
|
||||
"net10.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.5, )",
|
||||
"resolved": "10.0.5",
|
||||
"contentHash": "A+5ZuQ0f449tM+MQrhf6R9ZX7lYpjk/ODEwLYKrnF6111rtARx8fVsm4YznUnQiKnnXfaXNBqgxmil6RW3L3SA=="
|
||||
"requested": "[10.0.3, )",
|
||||
"resolved": "10.0.3",
|
||||
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||
},
|
||||
"sharpemu.core": {
|
||||
"type": "Project",
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -302,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}");
|
||||
@@ -342,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)
|
||||
|
||||
@@ -7,11 +7,14 @@ 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;
|
||||
|
||||
@@ -87,6 +90,16 @@ 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;
|
||||
@@ -105,6 +118,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private nint _tlsBaseAddress;
|
||||
|
||||
private nint _ownedTlsBaseAddress;
|
||||
|
||||
private bool _ownsTlsBaseAddress;
|
||||
|
||||
private int _tlsPatchStubOffset;
|
||||
@@ -197,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;
|
||||
@@ -219,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);
|
||||
@@ -336,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);
|
||||
@@ -376,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))
|
||||
@@ -407,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") + ") ===");
|
||||
}
|
||||
}
|
||||
@@ -645,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -669,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);
|
||||
@@ -966,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++;
|
||||
}
|
||||
@@ -1071,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
|
||||
{
|
||||
@@ -1087,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1268,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}");
|
||||
@@ -1315,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;
|
||||
@@ -1393,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)
|
||||
{
|
||||
@@ -1610,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)
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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,302 @@
|
||||
// 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 const int KernelEventSize = 0x20;
|
||||
|
||||
private static readonly object _eventQueueGate = new();
|
||||
private static readonly HashSet<ulong> _eventQueues = new();
|
||||
private static readonly Dictionary<ulong, Queue<KernelQueuedEvent>> _pendingEvents = new();
|
||||
private static long _nextEventQueueHandle = 1;
|
||||
|
||||
public readonly record struct KernelQueuedEvent(
|
||||
ulong Ident,
|
||||
short Filter,
|
||||
ushort Flags,
|
||||
uint Fflags,
|
||||
ulong Data,
|
||||
ulong UserData);
|
||||
|
||||
[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);
|
||||
_pendingEvents[handle] = new Queue<KernelQueuedEvent>();
|
||||
}
|
||||
|
||||
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);
|
||||
_pendingEvents.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 handle = ctx[CpuRegister.Rdi];
|
||||
var eventsAddress = ctx[CpuRegister.Rsi];
|
||||
var eventCapacity = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
||||
var outCountAddress = ctx[CpuRegister.Rcx];
|
||||
var timeoutAddress = ctx[CpuRegister.R8];
|
||||
|
||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (deliveredCount > 0)
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-deliver", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue"))
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-block", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
TraceEventQueue(ctx, "wait", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
public static bool IsValidEqueue(ulong handle)
|
||||
{
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
return _eventQueues.Contains(handle);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
|
||||
{
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
if (!_eventQueues.Contains(handle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new Queue<KernelQueuedEvent>();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
queue.Enqueue(queuedEvent);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
|
||||
{
|
||||
if (eventsAddress == 0 || eventCapacity <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
KernelQueuedEvent[] events;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var count = Math.Min(eventCapacity, queue.Count);
|
||||
events = new KernelQueuedEvent[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
events[i] = queue.Dequeue();
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < events.Length; i++)
|
||||
{
|
||||
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return events.Length;
|
||||
}
|
||||
|
||||
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
|
||||
{
|
||||
Span<byte> eventBytes = stackalloc byte[KernelEventSize];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x00..], queuedEvent.Ident);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(eventBytes[0x08..], queuedEvent.Filter);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(eventBytes[0x0A..], queuedEvent.Flags);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(eventBytes[0x0C..], queuedEvent.Fflags);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x10..], queuedEvent.Data);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x18..], queuedEvent.UserData);
|
||||
return ctx.Memory.TryWrite(address, eventBytes);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
@@ -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",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,6 +18,7 @@ public static class KernelPthreadCompatExports
|
||||
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();
|
||||
@@ -237,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",
|
||||
@@ -286,6 +294,13 @@ public static class KernelPthreadCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
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",
|
||||
ExportName = "scePthreadCondattrInit",
|
||||
@@ -407,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;
|
||||
@@ -856,6 +888,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var spuriousWake = false;
|
||||
lock (state.SyncRoot)
|
||||
{
|
||||
state.Waiters++;
|
||||
@@ -870,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;
|
||||
}
|
||||
|
||||
@@ -886,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);
|
||||
@@ -896,7 +962,15 @@ 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;
|
||||
}
|
||||
|
||||
@@ -943,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
|
||||
|
||||
@@ -20,6 +20,7 @@ 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();
|
||||
@@ -28,9 +29,9 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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
|
||||
{
|
||||
@@ -236,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",
|
||||
@@ -249,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",
|
||||
@@ -271,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",
|
||||
@@ -612,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",
|
||||
@@ -855,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;
|
||||
}
|
||||
@@ -878,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;
|
||||
}
|
||||
@@ -907,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 _))
|
||||
@@ -914,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;
|
||||
}
|
||||
|
||||
@@ -1116,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;
|
||||
}
|
||||
|
||||
@@ -258,6 +258,8 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
TraceProcParam(ctx, address);
|
||||
|
||||
ctx[CpuRegister.Rax] = address;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -270,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",
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
private const int MaxSemaphoreNameLength = 128;
|
||||
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
|
||||
private static int _nextSemaphoreHandle = 1;
|
||||
|
||||
private sealed class KernelSemaphoreState
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required int InitialCount { get; init; }
|
||||
public required int MaxCount { get; init; }
|
||||
public int Count { get; set; }
|
||||
public int WaitingThreads { get; set; }
|
||||
public object Gate { get; } = new();
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "188x57JYp0g",
|
||||
ExportName = "sceKernelCreateSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelCreateSema(CpuContext ctx)
|
||||
{
|
||||
var semaphoreAddress = ctx[CpuRegister.Rdi];
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
var attr = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||
var initialCount = unchecked((int)ctx[CpuRegister.Rcx]);
|
||||
var maxCount = unchecked((int)ctx[CpuRegister.R8]);
|
||||
var optionAddress = ctx[CpuRegister.R9];
|
||||
|
||||
if (semaphoreAddress == 0 ||
|
||||
nameAddress == 0 ||
|
||||
attr > 2 ||
|
||||
initialCount < 0 ||
|
||||
maxCount <= 0 ||
|
||||
initialCount > maxCount ||
|
||||
optionAddress != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||
if (handle == 0)
|
||||
{
|
||||
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||
}
|
||||
|
||||
_semaphores[handle] = new KernelSemaphoreState
|
||||
{
|
||||
Name = name,
|
||||
InitialCount = initialCount,
|
||||
MaxCount = maxCount,
|
||||
Count = initialCount,
|
||||
};
|
||||
|
||||
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||
{
|
||||
_semaphores.TryRemove(handle, out _);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Zxa0VhQVTsk",
|
||||
ExportName = "sceKernelWaitSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelWaitSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var timeoutAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count >= needCount)
|
||||
{
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
if (!TryReadUInt32(ctx, timeoutAddress, out _))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitSema"))
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "12wOHk8ywb0",
|
||||
ExportName = "sceKernelPollSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelPollSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count < needCount)
|
||||
{
|
||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
}
|
||||
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4czppHBiriw",
|
||||
ExportName = "sceKernelSignalSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelSignalSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var signalCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (signalCount <= 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count > semaphore.MaxCount - signalCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
semaphore.Count += signalCount;
|
||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4DM06U2BNEY",
|
||||
ExportName = "sceKernelCancelSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelCancelSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var setCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var waitingThreadsAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (setCount > semaphore.MaxCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
||||
semaphore.WaitingThreads = 0;
|
||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "R1Jvn8bSCW8",
|
||||
ExportName = "sceKernelDeleteSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelDeleteSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
if (!_semaphores.TryRemove(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
var value = (int)result;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (address == 0 || maxLength <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[Math.Min(maxLength, 4096)];
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address + (ulong)i, current))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, i);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes[i] = current[0];
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceSemaphore(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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.Diagnostics;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
public static class PadExports
|
||||
{
|
||||
private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003);
|
||||
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
||||
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
||||
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
||||
private const int PrimaryUserId = 1;
|
||||
private const int StandardPortType = 0;
|
||||
private const int PrimaryPadHandle = 1;
|
||||
private const int ControllerInformationSize = 0x1C;
|
||||
private const int PadDataSize = 0x78;
|
||||
|
||||
private static bool _initialized;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hv1luiJrqQM",
|
||||
ExportName = "scePadInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadInit(CpuContext ctx)
|
||||
{
|
||||
_initialized = true;
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xk0AcarP3V4",
|
||||
ExportName = "scePadOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadOpen(CpuContext ctx)
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var index = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var parameterAddress = ctx[CpuRegister.Rcx];
|
||||
if (!_initialized)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorNotInitialized);
|
||||
}
|
||||
|
||||
if (userId == -1)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
|
||||
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorDeviceNotConnected);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, PrimaryPadHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "clVvL4ZDntw",
|
||||
ExportName = "scePadSetMotionSensorState",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadSetMotionSensorState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return handle == PrimaryPadHandle
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gjP9-KQzoUk",
|
||||
ExportName = "scePadGetControllerInformation",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadGetControllerInformation(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (informationAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> information = stackalloc byte[ControllerInformationSize];
|
||||
BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
|
||||
information[0x08] = 30;
|
||||
information[0x09] = 30;
|
||||
information[0x0A] = StandardPortType;
|
||||
information[0x0B] = 1;
|
||||
information[0x0C] = 1;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
|
||||
|
||||
return ctx.Memory.TryWrite(informationAddress, information)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "YndgXqQVV7c",
|
||||
ExportName = "scePadReadState",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadReadState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (dataAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> data = stackalloc byte[PadDataSize];
|
||||
data.Clear();
|
||||
data[0x04] = 128;
|
||||
data[0x05] = 128;
|
||||
data[0x06] = 128;
|
||||
data[0x07] = 128;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||
data[0x4C] = 1;
|
||||
var timestampTicks = Stopwatch.GetTimestamp();
|
||||
var timestampMicroseconds =
|
||||
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
|
||||
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(
|
||||
data[0x50..],
|
||||
timestampMicroseconds);
|
||||
data[0x68] = 1;
|
||||
|
||||
return ctx.Memory.TryWrite(dataAddress, data)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.SystemService;
|
||||
|
||||
public static class SystemServiceExports
|
||||
{
|
||||
private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003);
|
||||
private const int SystemServiceStatusSize = 0x0C;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rPo6tV8D9bM",
|
||||
ExportName = "sceSystemServiceGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceGetStatus(CpuContext ctx)
|
||||
{
|
||||
var statusAddress = ctx[CpuRegister.Rdi];
|
||||
if (statusAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> status = stackalloc byte[SystemServiceStatusSize];
|
||||
status.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status, 0);
|
||||
status[0x06] = 1;
|
||||
|
||||
return ctx.Memory.TryWrite(statusAddress, status)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.UserService;
|
||||
|
||||
public static class UserServiceExports
|
||||
{
|
||||
private const int OrbisUserServiceErrorInvalidArgument = unchecked((int)0x80960005);
|
||||
private const int PrimaryUserId = 1;
|
||||
private const int InvalidUserId = -1;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "j3YMu1MVNNo",
|
||||
ExportName = "sceUserServiceInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceInitialize(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "CdWp0oHWGr0",
|
||||
ExportName = "sceUserServiceGetInitialUser",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceGetInitialUser(CpuContext ctx)
|
||||
{
|
||||
var userIdAddress = ctx[CpuRegister.Rdi];
|
||||
if (userIdAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fPhymKNvK-A",
|
||||
ExportName = "sceUserServiceGetLoginUserIdList",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceGetLoginUserIdList(CpuContext ctx)
|
||||
{
|
||||
var userIdListAddress = ctx[CpuRegister.Rdi];
|
||||
if (userIdListAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x00..], PrimaryUserId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x04..], InvalidUserId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
|
||||
return ctx.Memory.TryWrite(userIdListAddress, userIds)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
@@ -15,28 +16,79 @@ public static class VideoOutExports
|
||||
private const int OrbisVideoOutErrorResourceBusy = unchecked((int)0x80290009);
|
||||
private const int OrbisVideoOutErrorInvalidIndex = unchecked((int)0x8029000A);
|
||||
private const int OrbisVideoOutErrorInvalidHandle = unchecked((int)0x8029000B);
|
||||
private const int OrbisVideoOutErrorInvalidEventQueue = unchecked((int)0x8029000C);
|
||||
private const int OrbisVideoOutErrorInvalidEvent = unchecked((int)0x8029000D);
|
||||
private const int OrbisVideoOutErrorInvalidOption = unchecked((int)0x8029001A);
|
||||
private const int SceVideoOutBusTypeMain = 0;
|
||||
private const int SceVideoOutBufferAttributeOptionNone = 0;
|
||||
private const int SceVideoOutTilingModeLinear = 1;
|
||||
private const int MaxOpenPorts = 4;
|
||||
private const int MaxDisplayBuffers = 16;
|
||||
private const int VideoOutBufferAttributeSize = 0x24;
|
||||
private const int MaxDisplayBufferGroups = 4;
|
||||
private const int MaxFrameDumps = 8;
|
||||
private const int VideoOutBufferAttributeSize = 0x28;
|
||||
private const int VideoOutBufferAttribute2Size = 0x50;
|
||||
private const int VideoOutBuffersEntrySize = 0x20;
|
||||
private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
|
||||
private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
|
||||
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
|
||||
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
|
||||
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
|
||||
private const ulong SceVideoOutInternalEventFlip = 0x6;
|
||||
private const short OrbisKernelEventFilterVideoOut = -13;
|
||||
|
||||
private static readonly object _stateGate = new();
|
||||
private static readonly object _frameDumpGate = new();
|
||||
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
|
||||
private static readonly Dictionary<(int Handle, int BufferIndex, ulong Address), ulong> _lastFrameFingerprints = new();
|
||||
private static int _nextHandle = 1;
|
||||
private static int _frameDumpCount;
|
||||
private static long _nextFrameDumpIndex;
|
||||
|
||||
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();
|
||||
public ulong FlipCount { get; set; }
|
||||
public int CurrentBuffer { get; set; } = -1;
|
||||
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
|
||||
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
|
||||
public List<FlipEventRegistration> FlipEvents { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class VideoOutBufferGroup
|
||||
{
|
||||
public required int Index { get; init; }
|
||||
public required BufferAttribute Attribute { get; init; }
|
||||
}
|
||||
|
||||
private sealed class VideoOutBufferSlot
|
||||
{
|
||||
public int GroupIndex { get; set; } = -1;
|
||||
public ulong AddressLeft { get; set; }
|
||||
public ulong AddressRight { get; set; }
|
||||
}
|
||||
|
||||
private readonly record struct FlipEventRegistration(ulong Equeue, ulong UserData);
|
||||
|
||||
private readonly record struct BufferAttribute(
|
||||
ulong PixelFormat,
|
||||
uint TilingMode,
|
||||
uint AspectRatio,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint PitchInPixel,
|
||||
ulong Option);
|
||||
|
||||
internal readonly record struct DisplayBufferInfo(
|
||||
ulong Address,
|
||||
ulong PixelFormat,
|
||||
uint TilingMode,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint PitchInPixel);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Up36PTk687E",
|
||||
ExportName = "sceVideoOutOpen",
|
||||
@@ -136,6 +188,146 @@ public static class VideoOutExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "HXzjK9yI30k",
|
||||
ExportName = "sceVideoOutAddFlipEvent",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutAddFlipEvent(CpuContext ctx)
|
||||
{
|
||||
var equeue = ctx[CpuRegister.Rdi];
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var userData = ctx[CpuRegister.Rdx];
|
||||
if (!TryGetPort(handle, out var port))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
if (!KernelEventQueueCompatExports.IsValidEqueue(equeue))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidEventQueue;
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
port.FlipEvents.Add(new FlipEventRegistration(equeue, userData));
|
||||
}
|
||||
|
||||
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "U46NwOiJpys",
|
||||
ExportName = "sceVideoOutSubmitFlip",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutSubmitFlip(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var bufferIndex = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var flipMode = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var flipArg = unchecked((long)ctx[CpuRegister.Rcx]);
|
||||
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "U2JJtSqNKZI",
|
||||
ExportName = "sceVideoOutGetEventId",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutGetEventId(CpuContext ctx)
|
||||
{
|
||||
var eventAddress = ctx[CpuRegister.Rdi];
|
||||
if (eventAddress == 0)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidAddress;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(eventAddress, out var ident) ||
|
||||
!TryReadInt16(ctx, eventAddress + 0x08, out var filter))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidEvent;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rWUTcKdkUzQ",
|
||||
ExportName = "sceVideoOutGetEventData",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutGetEventData(CpuContext ctx)
|
||||
{
|
||||
var eventAddress = ctx[CpuRegister.Rdi];
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
if (eventAddress == 0 || dataAddress == 0)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidAddress;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(eventAddress, out var ident) ||
|
||||
!TryReadInt16(ctx, eventAddress + 0x08, out var filter) ||
|
||||
!ctx.TryReadUInt64(eventAddress + 0x10, out var data))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidEvent;
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(dataAddress, data >> 16)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
public static int SubmitFlipFromAgc(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg) =>
|
||||
SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
|
||||
|
||||
internal static bool TryGetDisplayBufferInfo(int handle, int bufferIndex, out DisplayBufferInfo info)
|
||||
{
|
||||
info = default;
|
||||
if (bufferIndex < 0 || bufferIndex >= MaxDisplayBuffers)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (!_ports.TryGetValue(handle, out var port))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var slot = port.BufferSlots[bufferIndex];
|
||||
if (slot.AddressLeft == 0 ||
|
||||
slot.GroupIndex < 0 ||
|
||||
slot.GroupIndex >= port.Groups.Length ||
|
||||
port.Groups[slot.GroupIndex] is not { } group)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var attribute = group.Attribute;
|
||||
info = new DisplayBufferInfo(
|
||||
slot.AddressLeft,
|
||||
attribute.PixelFormat,
|
||||
attribute.TilingMode,
|
||||
attribute.Width,
|
||||
attribute.Height,
|
||||
attribute.PitchInPixel);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MTxxrOCeSig",
|
||||
ExportName = "sceVideoOutSetWindowModeMargins",
|
||||
@@ -173,9 +365,23 @@ public static class VideoOutExports
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
return port.RegisteredSetIds.Remove(attributeIndex)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: OrbisVideoOutErrorInvalidValue;
|
||||
if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
port.Groups[attributeIndex] = null;
|
||||
foreach (var slot in port.BufferSlots)
|
||||
{
|
||||
if (slot.GroupIndex == attributeIndex)
|
||||
{
|
||||
slot.GroupIndex = -1;
|
||||
slot.AddressLeft = 0;
|
||||
slot.AddressRight = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,16 +498,21 @@ public static class VideoOutExports
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
if (!TryReadBufferAttribute(ctx, attributeAddress, false, out var attribute))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
Span<ulong> addresses = stackalloc ulong[Math.Min(bufferNum, MaxDisplayBuffers)];
|
||||
for (var i = 0; i < bufferNum; i++)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out _))
|
||||
if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out addresses[i]))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
|
||||
var setId = AllocateBufferSet(port);
|
||||
return setId;
|
||||
return RegisterBufferRange(port, startIndex, addresses[..bufferNum], attribute);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -348,34 +559,458 @@ public static class VideoOutExports
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
if (!TryReadBufferAttribute(ctx, attributeAddress, true, out var attribute))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
Span<ulong> addresses = stackalloc ulong[Math.Min(bufferNum, MaxDisplayBuffers)];
|
||||
for (var i = 0; i < bufferNum; i++)
|
||||
{
|
||||
var entryAddress = buffersAddress + ((ulong)i * VideoOutBuffersEntrySize);
|
||||
if (!ctx.TryReadUInt64(entryAddress + 0x00, out _) ||
|
||||
if (!ctx.TryReadUInt64(entryAddress + 0x00, out addresses[i]) ||
|
||||
!ctx.TryReadUInt64(entryAddress + 0x08, out _))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
port.RegisteredSetIds.Add(setIndex);
|
||||
}
|
||||
|
||||
return setIndex;
|
||||
var groupIndex = RegisterBufferRange(port, bufferIndexStart, addresses[..bufferNum], attribute, setIndex);
|
||||
return groupIndex < 0 ? groupIndex : setIndex;
|
||||
}
|
||||
|
||||
private static int AllocateBufferSet(VideoOutPortState port)
|
||||
private static int SubmitFlip(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg)
|
||||
{
|
||||
if (!TryGetPort(handle, out var port))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
if (bufferIndex < -1 || bufferIndex >= MaxDisplayBuffers)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidIndex;
|
||||
}
|
||||
|
||||
ulong eventData;
|
||||
List<FlipEventRegistration> flipEvents;
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidIndex;
|
||||
}
|
||||
|
||||
port.CurrentBuffer = bufferIndex;
|
||||
port.FlipCount++;
|
||||
var eventCount = Math.Min(port.FlipCount, 0xFUL);
|
||||
var timeBits = (ulong)Environment.TickCount64 & 0xFFFUL;
|
||||
eventData = timeBits | (eventCount << 12) | ((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
|
||||
}
|
||||
|
||||
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
||||
|
||||
foreach (var flipEvent in flipEvents)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.EnqueueEvent(
|
||||
flipEvent.Equeue,
|
||||
new KernelEventQueueCompatExports.KernelQueuedEvent(
|
||||
SceVideoOutInternalEventFlip,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
0,
|
||||
0,
|
||||
eventData,
|
||||
flipEvent.UserData));
|
||||
}
|
||||
|
||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
var setId = port.NextSetId++;
|
||||
port.RegisteredSetIds.Add(setId);
|
||||
return setId;
|
||||
var groupIndex = requestedGroupIndex >= 0 ? requestedGroupIndex : FindFreeGroupIndex(port);
|
||||
if (groupIndex < 0 || groupIndex >= MaxDisplayBufferGroups)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
if (port.Groups[groupIndex] is not null)
|
||||
{
|
||||
return OrbisVideoOutErrorResourceBusy;
|
||||
}
|
||||
|
||||
for (var i = 0; i < addresses.Length; i++)
|
||||
{
|
||||
if (port.BufferSlots[startIndex + i].GroupIndex >= 0)
|
||||
{
|
||||
return OrbisVideoOutErrorResourceBusy;
|
||||
}
|
||||
}
|
||||
|
||||
port.Groups[groupIndex] = new VideoOutBufferGroup
|
||||
{
|
||||
Index = groupIndex,
|
||||
Attribute = attribute,
|
||||
};
|
||||
|
||||
for (var i = 0; i < addresses.Length; i++)
|
||||
{
|
||||
var slot = port.BufferSlots[startIndex + i];
|
||||
slot.GroupIndex = groupIndex;
|
||||
slot.AddressLeft = addresses[i];
|
||||
slot.AddressRight = 0;
|
||||
}
|
||||
|
||||
TraceVideoOut(
|
||||
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||
return groupIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private static int FindFreeGroupIndex(VideoOutPortState port)
|
||||
{
|
||||
for (var i = 0; i < port.Groups.Length; i++)
|
||||
{
|
||||
if (port.Groups[i] is null)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static bool TryReadBufferAttribute(CpuContext ctx, ulong attributeAddress, bool attribute2, out BufferAttribute attribute)
|
||||
{
|
||||
attribute = default;
|
||||
if (!TryReadUInt32(ctx, attributeAddress + 0x04, out var tilingMode) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x0C, out var width) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x10, out var height))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attribute2)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(attributeAddress + 0x18, out var option) ||
|
||||
!ctx.TryReadUInt64(attributeAddress + 0x20, out var pixelFormat))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
attribute = new BufferAttribute(NormalizePixelFormat(pixelFormat), tilingMode, 0, width, height, width, option);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryReadUInt32(ctx, attributeAddress + 0x00, out var pixelFormat32) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x08, out var aspectRatio) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x14, out var pitchInPixel) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x18, out var option32))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
attribute = new BufferAttribute(NormalizePixelFormat(pixelFormat32), tilingMode, aspectRatio, width, height, pitchInPixel, option32);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryDumpFrame(CpuContext ctx, VideoOutPortState port, int bufferIndex, int flipMode, long flipArg)
|
||||
{
|
||||
if (bufferIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
VideoOutBufferSlot slot;
|
||||
VideoOutBufferGroup? group;
|
||||
lock (_stateGate)
|
||||
{
|
||||
slot = port.BufferSlots[bufferIndex];
|
||||
group = slot.GroupIndex >= 0 && slot.GroupIndex < port.Groups.Length
|
||||
? port.Groups[slot.GroupIndex]
|
||||
: null;
|
||||
}
|
||||
|
||||
if (group is null || slot.AddressLeft == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var attribute = group.Attribute;
|
||||
if (attribute.Width == 0 || attribute.Height == 0 || attribute.Width > 8192 || attribute.Height > 8192)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytesPerPixel = GetBytesPerPixel(attribute.PixelFormat);
|
||||
if (bytesPerPixel == 0)
|
||||
{
|
||||
return DumpRawFrame(ctx, port.Handle, slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "unsupported-format");
|
||||
}
|
||||
|
||||
var pitch = attribute.PitchInPixel == 0 ? attribute.Width : attribute.PitchInPixel;
|
||||
var rowBytes = checked((int)(pitch * bytesPerPixel));
|
||||
var visibleRowBytes = checked((int)(attribute.Width * bytesPerPixel));
|
||||
var frameBytes = checked((ulong)rowBytes * attribute.Height);
|
||||
if (frameBytes > 256UL * 1024UL * 1024UL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_frameDumpGate)
|
||||
{
|
||||
if (_frameDumpCount >= MaxFrameDumps)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const ulong fnvOffsetBasis = 14695981039346656037UL;
|
||||
const ulong fnvPrime = 1099511628211UL;
|
||||
var fingerprint = fnvOffsetBasis;
|
||||
var row = new byte[rowBytes];
|
||||
for (uint y = 0; y < attribute.Height; y++)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(slot.AddressLeft + ((ulong)y * (ulong)rowBytes), row))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var value in row.AsSpan(0, visibleRowBytes))
|
||||
{
|
||||
fingerprint = (fingerprint ^ value) * fnvPrime;
|
||||
}
|
||||
}
|
||||
|
||||
var fingerprintKey = (port.Handle, bufferIndex, slot.AddressLeft);
|
||||
lock (_frameDumpGate)
|
||||
{
|
||||
if (_lastFrameFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) &&
|
||||
previousFingerprint == fingerprint)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_frameDumpCount >= MaxFrameDumps)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_lastFrameFingerprints[fingerprintKey] = fingerprint;
|
||||
_frameDumpCount++;
|
||||
}
|
||||
|
||||
var rgb = new byte[checked((int)(attribute.Width * attribute.Height * 3))];
|
||||
var rgbOffset = 0;
|
||||
for (uint y = 0; y < attribute.Height; y++)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(slot.AddressLeft + ((ulong)y * (ulong)rowBytes), row))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ConvertRowToRgb(row.AsSpan(0, visibleRowBytes), rgb.AsSpan(rgbOffset, (int)attribute.Width * 3), attribute.PixelFormat);
|
||||
rgbOffset += (int)attribute.Width * 3;
|
||||
}
|
||||
|
||||
var frameIndex = Interlocked.Increment(ref _nextFrameDumpIndex);
|
||||
var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex);
|
||||
WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb);
|
||||
WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint);
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DumpRawFrame(CpuContext ctx, int handle, ulong address, BufferAttribute attribute, int bufferIndex, int flipMode, long flipArg, string reason)
|
||||
{
|
||||
var bytesPerPixel = Math.Max(GetBytesPerPixel(attribute.PixelFormat), 4u);
|
||||
var pitch = attribute.PitchInPixel == 0 ? attribute.Width : attribute.PitchInPixel;
|
||||
var byteCount = checked((ulong)pitch * attribute.Height * bytesPerPixel);
|
||||
if (byteCount == 0 || byteCount > 256UL * 1024UL * 1024UL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[(int)byteCount];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var fingerprint = ComputeFingerprint(bytes);
|
||||
var fingerprintKey = (handle, bufferIndex, address);
|
||||
lock (_frameDumpGate)
|
||||
{
|
||||
if ((_lastFrameFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) &&
|
||||
previousFingerprint == fingerprint) ||
|
||||
_frameDumpCount >= MaxFrameDumps)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_lastFrameFingerprints[fingerprintKey] = fingerprint;
|
||||
_frameDumpCount++;
|
||||
}
|
||||
|
||||
var frameIndex = Interlocked.Increment(ref _nextFrameDumpIndex);
|
||||
var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex);
|
||||
File.WriteAllBytes(basePath + ".raw", bytes);
|
||||
WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint);
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong ComputeFingerprint(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
const ulong fnvOffsetBasis = 14695981039346656037UL;
|
||||
const ulong fnvPrime = 1099511628211UL;
|
||||
var fingerprint = fnvOffsetBasis;
|
||||
foreach (var value in bytes)
|
||||
{
|
||||
fingerprint = (fingerprint ^ value) * fnvPrime;
|
||||
}
|
||||
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
private static uint GetBytesPerPixel(ulong pixelFormat) =>
|
||||
pixelFormat is SceVideoOutPixelFormatA8R8G8B8Srgb or
|
||||
SceVideoOutPixelFormatA8B8G8R8Srgb or
|
||||
SceVideoOutPixelFormatA2R10G10B10 or
|
||||
SceVideoOutPixelFormatA2R10G10B10Srgb or
|
||||
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq
|
||||
? 4u
|
||||
: 0u;
|
||||
|
||||
private static ulong NormalizePixelFormat(ulong pixelFormat)
|
||||
{
|
||||
if (GetBytesPerPixel(pixelFormat) != 0)
|
||||
{
|
||||
return pixelFormat;
|
||||
}
|
||||
|
||||
var low = (uint)(pixelFormat & 0xFFFF_FFFFUL);
|
||||
if (GetBytesPerPixel(low) != 0)
|
||||
{
|
||||
return low;
|
||||
}
|
||||
|
||||
var high = (uint)(pixelFormat >> 32);
|
||||
if (GetBytesPerPixel(high) != 0)
|
||||
{
|
||||
return high;
|
||||
}
|
||||
|
||||
var packed = high | (low >> 16);
|
||||
return GetBytesPerPixel(packed) != 0 ? packed : pixelFormat;
|
||||
}
|
||||
|
||||
private static void ConvertRowToRgb(ReadOnlySpan<byte> source, Span<byte> destination, ulong pixelFormat)
|
||||
{
|
||||
var dst = 0;
|
||||
for (var src = 0; src + 3 < source.Length; src += 4)
|
||||
{
|
||||
if (pixelFormat == SceVideoOutPixelFormatA8B8G8R8Srgb)
|
||||
{
|
||||
destination[dst++] = source[src + 0];
|
||||
destination[dst++] = source[src + 1];
|
||||
destination[dst++] = source[src + 2];
|
||||
}
|
||||
else if (pixelFormat is SceVideoOutPixelFormatA2R10G10B10 or SceVideoOutPixelFormatA2R10G10B10Srgb or SceVideoOutPixelFormatA2R10G10B10Bt2020Pq)
|
||||
{
|
||||
var value = BinaryPrimitives.ReadUInt32LittleEndian(source[src..(src + 4)]);
|
||||
destination[dst++] = (byte)(((value >> 20) & 0x3FF) >> 2);
|
||||
destination[dst++] = (byte)(((value >> 10) & 0x3FF) >> 2);
|
||||
destination[dst++] = (byte)((value & 0x3FF) >> 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
destination[dst++] = source[src + 2];
|
||||
destination[dst++] = source[src + 1];
|
||||
destination[dst++] = source[src + 0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFrameDumpBasePath(long frameIndex, int handle, int bufferIndex)
|
||||
{
|
||||
var directory = GetLogsDirectory();
|
||||
Directory.CreateDirectory(directory);
|
||||
return Path.Combine(directory, $"videoout_frame_{frameIndex:D4}_h{handle}_b{bufferIndex}");
|
||||
}
|
||||
|
||||
private static string GetLogsDirectory()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "SharpEmu.slnx")))
|
||||
{
|
||||
return Path.Combine(current.FullName, "logs");
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
return Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
||||
}
|
||||
|
||||
private static void WriteBmp(string path, uint width, uint height, byte[] rgb)
|
||||
{
|
||||
var rowStride = checked((int)(((width * 3u) + 3u) & ~3u));
|
||||
var pixelBytes = checked(rowStride * (int)height);
|
||||
var fileSize = 54 + pixelBytes;
|
||||
using var stream = File.Create(path);
|
||||
Span<byte> header = stackalloc byte[54];
|
||||
header[0] = (byte)'B';
|
||||
header[1] = (byte)'M';
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header[0x02..], (uint)fileSize);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header[0x0A..], 54);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header[0x0E..], 40);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(header[0x12..], (int)width);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(header[0x16..], -(int)height);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(header[0x1A..], 1);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(header[0x1C..], 24);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header[0x22..], (uint)pixelBytes);
|
||||
stream.Write(header);
|
||||
|
||||
var row = new byte[rowStride];
|
||||
var sourceStride = (int)width * 3;
|
||||
var heightInt = (int)height;
|
||||
var widthInt = (int)width;
|
||||
for (var y = 0; y < heightInt; y++)
|
||||
{
|
||||
row.AsSpan().Clear();
|
||||
var src = rgb.AsSpan(y * sourceStride, sourceStride);
|
||||
for (var x = 0; x < widthInt; x++)
|
||||
{
|
||||
row[(x * 3) + 0] = src[(x * 3) + 2];
|
||||
row[(x * 3) + 1] = src[(x * 3) + 1];
|
||||
row[(x * 3) + 2] = src[(x * 3) + 0];
|
||||
}
|
||||
|
||||
stream.Write(row);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteFrameMetadata(
|
||||
string path,
|
||||
ulong address,
|
||||
BufferAttribute attribute,
|
||||
int bufferIndex,
|
||||
int flipMode,
|
||||
long flipArg,
|
||||
string kind,
|
||||
ulong fingerprint)
|
||||
{
|
||||
File.WriteAllText(
|
||||
path,
|
||||
$"kind={kind}\naddress=0x{address:X16}\nbuffer_index={bufferIndex}\nflip_mode={flipMode}\nflip_arg={flipArg}\nfingerprint=0x{fingerprint:X16}\npixel_format=0x{attribute.PixelFormat:X}\ntiling_mode={attribute.TilingMode}\nwidth={attribute.Width}\nheight={attribute.Height}\npitch_in_pixel={attribute.PitchInPixel}\noption=0x{attribute.Option:X}\n");
|
||||
}
|
||||
|
||||
private static bool IsValidBufferRange(int startIndex, int bufferNum)
|
||||
{
|
||||
return startIndex >= 0 &&
|
||||
@@ -393,6 +1028,17 @@ public static class VideoOutExports
|
||||
}
|
||||
}
|
||||
|
||||
private static VideoOutBufferSlot[] CreateBufferSlots()
|
||||
{
|
||||
var slots = new VideoOutBufferSlot[MaxDisplayBuffers];
|
||||
for (var i = 0; i < slots.Length; i++)
|
||||
{
|
||||
slots[i] = new VideoOutBufferSlot();
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
private static bool TryReadStackUInt32(CpuContext ctx, int stackIndex, out uint value)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rsp] + 0x08 + ((ulong)stackIndex * 0x08);
|
||||
@@ -406,4 +1052,40 @@ public static class VideoOutExports
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadInt16(CpuContext ctx, ulong address, out short value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(short)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt16LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceVideoOut(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user