mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| daeff21516 | |||
| 82619deb34 | |||
| 697ad7be80 | |||
| 73c987e49f | |||
| cd79275117 | |||
| b14ecae504 | |||
| 873f473b65 | |||
| 9a1a3789ef | |||
| 0f3d4032cb | |||
| 0c4a757695 | |||
| 5fcc8eaa37 | |||
| 21105e0346 | |||
| b424df5a64 | |||
| 7f85971e39 | |||
| e28259a99d | |||
| 3d2a4b74ac | |||
| af35389bfd | |||
| 4ae778ef39 | |||
| a04e70c0b3 | |||
| 02222c7919 | |||
| 46b3a207c1 | |||
| 418d46beb5 | |||
| 0e922a73ee | |||
| a5172fd2c0 | |||
| 4ab614e68a | |||
| e581fe41f4 | |||
| 0f0ec9a020 | |||
| 70048cb49f | |||
| 4777ec4544 | |||
| caee2ad692 | |||
| 7c83f7f925 | |||
| 0e37bc95e9 | |||
| 318425630a | |||
| 992ef68ba8 | |||
| 01ebab90c0 | |||
| d134f9b9f6 | |||
| dad85b87c3 | |||
| 464422f23e | |||
| 5afa5e1ffa | |||
| 5512da9e12 | |||
| 2a52ccfe6a | |||
| 08b315b5fc |
Binary file not shown.
|
After Width: | Height: | Size: 933 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 621 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
@@ -9,11 +9,17 @@ on:
|
|||||||
- "**"
|
- "**"
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**/*.md"
|
- "**/*.md"
|
||||||
|
- "**/*.png"
|
||||||
|
- "**/*.jpeg"
|
||||||
|
- "**/*.jpg"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**/*.md"
|
- "**/*.md"
|
||||||
|
- "**/*.png"
|
||||||
|
- "**/*.jpeg"
|
||||||
|
- "**/*.jpg"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
private const ulong StackSize = 0x0020_0000UL;
|
private const ulong StackSize = 0x0020_0000UL;
|
||||||
private const ulong TlsBaseAddress = 0x7FFE_0000_0000UL;
|
private const ulong TlsBaseAddress = 0x7FFE_0000_0000UL;
|
||||||
private const ulong TlsSize = 0x0001_0000UL;
|
private const ulong TlsSize = 0x0001_0000UL;
|
||||||
|
private const ulong TlsPrefixSize = 0x0000_1000UL;
|
||||||
private const ulong BootstrapStubBaseAddress = 0x7FFD_F000_0000UL;
|
private const ulong BootstrapStubBaseAddress = 0x7FFD_F000_0000UL;
|
||||||
private const ulong BootstrapPayloadBaseAddress = 0x7FFD_E000_0000UL;
|
private const ulong BootstrapPayloadBaseAddress = 0x7FFD_E000_0000UL;
|
||||||
private const ulong DynlibFallbackStubBaseAddress = 0x7FFD_D000_0000UL;
|
private const ulong DynlibFallbackStubBaseAddress = 0x7FFD_D000_0000UL;
|
||||||
@@ -342,11 +343,12 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
for (var i = 0; i < 32; i++)
|
for (var i = 0; i < 32; i++)
|
||||||
{
|
{
|
||||||
var candidateBase = TlsBaseAddress - ((ulong)i * tlsStride);
|
var candidateBase = TlsBaseAddress - ((ulong)i * tlsStride);
|
||||||
|
var mappedBase = candidateBase - TlsPrefixSize;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_virtualMemory.Map(
|
_virtualMemory.Map(
|
||||||
candidateBase,
|
mappedBase,
|
||||||
TlsSize,
|
TlsSize + TlsPrefixSize,
|
||||||
fileOffset: 0,
|
fileOffset: 0,
|
||||||
fileData: ReadOnlySpan<byte>.Empty,
|
fileData: ReadOnlySpan<byte>.Empty,
|
||||||
ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||||
@@ -363,9 +365,11 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
|
|
||||||
private static bool InitializeTls(CpuContext context, ulong tlsBase)
|
private static bool InitializeTls(CpuContext context, ulong tlsBase)
|
||||||
{
|
{
|
||||||
return context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
|
return context.TryWriteUInt64(tlsBase - 0xF0, 0) &&
|
||||||
|
context.TryWriteUInt64(tlsBase + 0x00, tlsBase) &&
|
||||||
context.TryWriteUInt64(tlsBase + 0x10, tlsBase) &&
|
context.TryWriteUInt64(tlsBase + 0x10, tlsBase) &&
|
||||||
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL);
|
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL) &&
|
||||||
|
context.TryWriteUInt64(tlsBase + 0x60, tlsBase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
|
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
|
||||||
@@ -420,6 +424,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var entryStackPointer = entryParamsAddress - sizeof(ulong);
|
||||||
|
if (!context.TryWriteUInt64(entryStackPointer, 0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
context[CpuRegister.Rsp] = entryStackPointer;
|
||||||
context[CpuRegister.Rdi] = entryParamsAddress;
|
context[CpuRegister.Rdi] = entryParamsAddress;
|
||||||
context[CpuRegister.Rsi] = programExitHandlerAddress;
|
context[CpuRegister.Rsi] = programExitHandlerAddress;
|
||||||
context[CpuRegister.Rdx] = 0;
|
context[CpuRegister.Rdx] = 0;
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
@@ -12,10 +13,23 @@ namespace SharpEmu.Core.Cpu.Native;
|
|||||||
|
|
||||||
public sealed partial class DirectExecutionBackend
|
public sealed partial class DirectExecutionBackend
|
||||||
{
|
{
|
||||||
|
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
|
||||||
|
|
||||||
private void RecordRecentImportTrace(string traceLine)
|
private void RecordRecentImportTrace(
|
||||||
|
long dispatchIndex,
|
||||||
|
string nid,
|
||||||
|
ulong returnRip,
|
||||||
|
ulong arg0,
|
||||||
|
ulong arg1,
|
||||||
|
ulong arg2)
|
||||||
{
|
{
|
||||||
_recentImportTrace[_recentImportTraceWriteIndex] = traceLine;
|
_recentImportTrace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
|
||||||
|
dispatchIndex,
|
||||||
|
nid,
|
||||||
|
returnRip,
|
||||||
|
arg0,
|
||||||
|
arg1,
|
||||||
|
arg2);
|
||||||
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
|
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
|
||||||
if (_recentImportTraceCount < _recentImportTrace.Length)
|
if (_recentImportTraceCount < _recentImportTrace.Length)
|
||||||
{
|
{
|
||||||
@@ -34,10 +48,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
for (int i = 0; i < _recentImportTraceCount; i++)
|
for (int i = 0; i < _recentImportTraceCount; i++)
|
||||||
{
|
{
|
||||||
int num2 = (num + i) % _recentImportTrace.Length;
|
int num2 = (num + i) % _recentImportTrace.Length;
|
||||||
string text = _recentImportTrace[num2];
|
var entry = _recentImportTrace[num2];
|
||||||
if (!string.IsNullOrEmpty(text))
|
if (!string.IsNullOrEmpty(entry.Nid))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][INFO] " + text);
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||||
|
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -97,12 +113,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
private void ProbeReturnRip(ulong returnRip, long dispatchIndex)
|
private void ProbeReturnRip(ulong returnRip, long dispatchIndex)
|
||||||
{
|
{
|
||||||
if (_cpuContext == null || returnRip == 0)
|
var cpuContext = ActiveCpuContext;
|
||||||
|
if (cpuContext == null || returnRip == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Span<byte> destination = stackalloc byte[128];
|
Span<byte> destination = stackalloc byte[128];
|
||||||
if (!_cpuContext.Memory.TryRead(returnRip, destination))
|
if (!cpuContext.Memory.TryRead(returnRip, destination))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
|
||||||
return;
|
return;
|
||||||
@@ -113,7 +130,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
int num = BitConverter.ToInt32(destination.Slice(2, 4));
|
int num = BitConverter.ToInt32(destination.Slice(2, 4));
|
||||||
ulong num2 = returnRip + 6 + (ulong)num;
|
ulong num2 = returnRip + 6 + (ulong)num;
|
||||||
if (_cpuContext.TryReadUInt64(num2, out var value2))
|
if (cpuContext.TryReadUInt64(num2, out var value2))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
|
||||||
}
|
}
|
||||||
@@ -122,7 +139,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
int num3 = BitConverter.ToInt32(destination.Slice(3, 4));
|
int num3 = BitConverter.ToInt32(destination.Slice(3, 4));
|
||||||
ulong num4 = returnRip + 7 + (ulong)num3;
|
ulong num4 = returnRip + 7 + (ulong)num3;
|
||||||
if (_cpuContext.TryReadUInt64(num4, out var value3))
|
if (cpuContext.TryReadUInt64(num4, out var value3))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
|
||||||
}
|
}
|
||||||
@@ -134,12 +151,82 @@ public sealed partial class DirectExecutionBackend
|
|||||||
int num5 = BitConverter.ToInt32(destination.Slice(i + 2, 4));
|
int num5 = BitConverter.ToInt32(destination.Slice(i + 2, 4));
|
||||||
ulong num6 = returnRip + (ulong)i;
|
ulong num6 = returnRip + (ulong)i;
|
||||||
ulong num7 = num6 + 6 + (ulong)num5;
|
ulong num7 = num6 + 6 + (ulong)num5;
|
||||||
if (_cpuContext.TryReadUInt64(num7, out var value4))
|
if (cpuContext.TryReadUInt64(num7, out var value4))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Span<byte> targetBytes = stackalloc byte[32];
|
||||||
|
for (int i = 0; i + 5 <= destination.Length; i++)
|
||||||
|
{
|
||||||
|
if (destination[i] != 0xE8)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
|
||||||
|
ulong callRip = returnRip + (ulong)i;
|
||||||
|
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
||||||
|
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||||
|
{
|
||||||
|
if (_importEntries[importIndex].Address != target)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string nid = _importEntries[importIndex].Nid;
|
||||||
|
if (_moduleManager.TryGetExport(nid, out var export))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cpuContext.Memory.TryRead(target, targetBytes))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
||||||
|
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
|
||||||
|
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
|
||||||
|
{
|
||||||
|
int slotRel32 = BitConverter.ToInt32(targetBytes.Slice(2, 4));
|
||||||
|
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
|
||||||
|
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||||
|
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||||
|
{
|
||||||
|
if (_importEntries[importIndex].Address != slotTarget)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
string nid = _importEntries[importIndex].Nid;
|
||||||
|
if (_moduleManager.TryGetExport(nid, out var export))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsUnresolvedSentinel(ulong value)
|
private static bool IsUnresolvedSentinel(ulong value)
|
||||||
@@ -231,11 +318,24 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
private unsafe static bool IsExecutableAddress(ulong address)
|
private unsafe static bool IsExecutableAddress(ulong address)
|
||||||
{
|
{
|
||||||
|
var pageAddress = address & ~0xFFFUL;
|
||||||
|
if (_knownExecutablePages.ContainsKey(pageAddress))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
|
||||||
|
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
||||||
|
if (executable)
|
||||||
|
{
|
||||||
|
_knownExecutablePages.TryAdd(pageAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return executable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong AlignUp(ulong value, ulong alignment)
|
private static ulong AlignUp(ulong value, ulong alignment)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
|||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu.Disasm;
|
using SharpEmu.Core.Cpu.Disasm;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
@@ -14,11 +15,19 @@ namespace SharpEmu.Core.Cpu.Native;
|
|||||||
|
|
||||||
public sealed partial class DirectExecutionBackend
|
public sealed partial class DirectExecutionBackend
|
||||||
{
|
{
|
||||||
|
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
|
||||||
|
private static int _lazyCommitTraceCount;
|
||||||
|
|
||||||
private unsafe void SetupExceptionHandler()
|
private unsafe void SetupExceptionHandler()
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, RawVectoredHandlerPtrManaged);
|
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
|
||||||
|
if (_rawExceptionHandlerStub == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
||||||
|
}
|
||||||
|
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -28,12 +37,22 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
_handlerDelegate = VectoredHandler;
|
_handlerDelegate = VectoredHandler;
|
||||||
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
||||||
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
||||||
|
if (_exceptionHandlerStub == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
||||||
|
}
|
||||||
|
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
||||||
|
|
||||||
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
||||||
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
||||||
SetUnhandledExceptionFilter(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
||||||
|
if (_unhandledFilterStub == 0)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
||||||
|
}
|
||||||
|
SetUnhandledExceptionFilter(_unhandledFilterStub);
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
||||||
@@ -61,7 +80,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
if (_vectoredHandlerDepth > 0)
|
if (_vectoredHandlerDepth > 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][TRACE] Nested VEH exception; passing through.");
|
LogNestedVectoredException(exceptionInfo);
|
||||||
Console.Error.Flush();
|
Console.Error.Flush();
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
@@ -84,10 +103,18 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||||
|
|
||||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord))
|
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
|
if (IsBenignHostDebugException(exceptionCode))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
if (exceptionCode == MSVC_CPP_EXCEPTION)
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
switch (exceptionCode)
|
switch (exceptionCode)
|
||||||
{
|
{
|
||||||
@@ -278,6 +305,37 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsBenignHostDebugException(uint exceptionCode)
|
||||||
|
{
|
||||||
|
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe static void LogNestedVectoredException(void* exceptionInfo)
|
||||||
|
{
|
||||||
|
int count = Interlocked.Increment(ref _nestedVehTraceCount);
|
||||||
|
if (count > 16 && count % 128 != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
EXCEPTION_POINTERS* pointers = (EXCEPTION_POINTERS*)exceptionInfo;
|
||||||
|
EXCEPTION_RECORD* record = pointers->ExceptionRecord;
|
||||||
|
void* contextRecord = pointers->ContextRecord;
|
||||||
|
ulong rip = contextRecord != null ? ReadCtxU64(contextRecord, 248) : 0;
|
||||||
|
ulong rsp = contextRecord != null ? ReadCtxU64(contextRecord, 152) : 0;
|
||||||
|
ulong accessType = record->NumberParameters >= 1 ? *record->ExceptionInformation : 0;
|
||||||
|
ulong target = record->NumberParameters >= 2 ? record->ExceptionInformation[1] : 0;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] Nested VEH exception#{count}: code=0x{record->ExceptionCode:X8} ex=0x{(ulong)record->ExceptionAddress:X16} rip=0x{rip:X16} rsp=0x{rsp:X16} type={accessType} target=0x{target:X16}; passing through.");
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] Nested VEH exception#{count}; passing through.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private unsafe void LogAccessViolationTrace(ulong exceptionAddress, EXCEPTION_RECORD* exceptionRecord)
|
private unsafe void LogAccessViolationTrace(ulong exceptionAddress, EXCEPTION_RECORD* exceptionRecord)
|
||||||
{
|
{
|
||||||
ulong accessType = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
ulong accessType = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
||||||
@@ -828,7 +886,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe static bool TryHandleLazyCommittedPage(EXCEPTION_RECORD* exceptionRecord)
|
private unsafe bool TryHandleLazyCommittedPage(EXCEPTION_RECORD* exceptionRecord, ulong rip, ulong rsp)
|
||||||
{
|
{
|
||||||
if (exceptionRecord->NumberParameters < 2)
|
if (exceptionRecord->NumberParameters < 2)
|
||||||
{
|
{
|
||||||
@@ -845,6 +903,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
if (!IsGuestOwnedLazyCommitAddress(faultAddress, out var owner))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -852,7 +914,21 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
||||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query: fault=0x{faultAddress:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
||||||
|
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
||||||
|
if (traceLazyCommit)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
|
||||||
|
{
|
||||||
|
if (traceLazyCommit)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool committed = false;
|
bool committed = false;
|
||||||
ulong committedBase = 0;
|
ulong committedBase = 0;
|
||||||
@@ -860,14 +936,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
if (mbi.State == 65536)
|
if (mbi.State == 65536)
|
||||||
{
|
{
|
||||||
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
||||||
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeBase;
|
committedBase = windowBase;
|
||||||
committedSize = 2097152uL;
|
committedSize = windowSize;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
|
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||||
|
{
|
||||||
|
committed = true;
|
||||||
|
committedBase = largeBase;
|
||||||
|
committedSize = 2097152uL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||||
@@ -890,7 +977,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
if (traceLazyCommit)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -899,14 +989,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
||||||
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeCommitBase;
|
committedBase = commitWindowBase;
|
||||||
committedSize = 2097152uL;
|
committedSize = commitWindowSize;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
|
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
||||||
|
{
|
||||||
|
committed = true;
|
||||||
|
committedBase = largeCommitBase;
|
||||||
|
committedSize = 2097152uL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
||||||
@@ -935,9 +1036,46 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
if (traceLazyCommit)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
static bool TryGetLazyCommitWindow(ulong fault, ulong regionBase, ulong regionSize, out ulong baseAddress, out ulong length)
|
||||||
|
{
|
||||||
|
baseAddress = 0;
|
||||||
|
length = 0;
|
||||||
|
if (regionSize == 0 || ulong.MaxValue - regionBase < regionSize)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong regionEnd = regionBase + regionSize;
|
||||||
|
ulong windowBase = fault & ~(LazyCommitWindowBytes - 1);
|
||||||
|
if (windowBase < regionBase)
|
||||||
|
{
|
||||||
|
windowBase = regionBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowBase >= regionEnd)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong windowEnd = Math.Min(regionEnd, windowBase + LazyCommitWindowBytes);
|
||||||
|
ulong windowSize = windowEnd - windowBase;
|
||||||
|
windowSize &= 0xFFFFFFFFFFFFF000uL;
|
||||||
|
if (windowSize == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
baseAddress = windowBase;
|
||||||
|
length = windowSize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
||||||
{
|
{
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
@@ -964,6 +1102,49 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
return TryCommitRange(commitAddress, commitSize, protection);
|
return TryCommitRange(commitAddress, commitSize, protection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool IsAccessCompatible(ulong accessType, uint protection)
|
||||||
|
{
|
||||||
|
const uint pageNoAccess = 0x01;
|
||||||
|
const uint pageReadOnly = 0x02;
|
||||||
|
const uint pageReadWrite = 0x04;
|
||||||
|
const uint pageWriteCopy = 0x08;
|
||||||
|
const uint pageExecute = 0x10;
|
||||||
|
const uint pageExecuteRead = 0x20;
|
||||||
|
const uint pageExecuteReadWrite = 0x40;
|
||||||
|
const uint pageExecuteWriteCopy = 0x80;
|
||||||
|
const uint pageGuard = 0x100;
|
||||||
|
const uint accessMask = 0xFF;
|
||||||
|
|
||||||
|
if ((protection & pageGuard) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint access = protection & accessMask;
|
||||||
|
if (access == pageNoAccess)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessType switch
|
||||||
|
{
|
||||||
|
0 => access is pageReadOnly or pageReadWrite or pageWriteCopy or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
|
||||||
|
1 => access is pageReadWrite or pageWriteCopy or pageExecuteReadWrite or pageExecuteWriteCopy,
|
||||||
|
8 => access is pageExecute or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTraceLazyCommit(int traceIndex)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LAZY_COMMIT"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return traceIndex <= 16 || traceIndex % 256 == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
|
private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -5,7 +5,7 @@ using SharpEmu.HLE;
|
|||||||
|
|
||||||
namespace SharpEmu.Core.Cpu;
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory
|
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator
|
||||||
{
|
{
|
||||||
private readonly ICpuMemory _inner;
|
private readonly ICpuMemory _inner;
|
||||||
|
|
||||||
@@ -39,4 +39,15 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory
|
|||||||
|
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
||||||
|
{
|
||||||
|
if (_inner is IGuestMemoryAllocator allocator)
|
||||||
|
{
|
||||||
|
return allocator.TryAllocateGuestMemory(size, alignment, out address);
|
||||||
|
}
|
||||||
|
|
||||||
|
address = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,10 @@ public sealed class SelfImage
|
|||||||
IReadOnlyList<ulong>? initializerFunctions = null,
|
IReadOnlyList<ulong>? initializerFunctions = null,
|
||||||
ulong initFunctionEntryPoint = 0,
|
ulong initFunctionEntryPoint = 0,
|
||||||
ulong imageBase = 0,
|
ulong imageBase = 0,
|
||||||
ulong procParamAddress = 0)
|
ulong procParamAddress = 0,
|
||||||
|
string? title = null,
|
||||||
|
string? titleId = null,
|
||||||
|
string? version = null)
|
||||||
{
|
{
|
||||||
ArgumentNullException.ThrowIfNull(programHeaders);
|
ArgumentNullException.ThrowIfNull(programHeaders);
|
||||||
ArgumentNullException.ThrowIfNull(mappedRegions);
|
ArgumentNullException.ThrowIfNull(mappedRegions);
|
||||||
@@ -38,6 +41,9 @@ public sealed class SelfImage
|
|||||||
InitFunctionEntryPoint = initFunctionEntryPoint;
|
InitFunctionEntryPoint = initFunctionEntryPoint;
|
||||||
_imageBase = imageBase;
|
_imageBase = imageBase;
|
||||||
ProcParamAddress = procParamAddress;
|
ProcParamAddress = procParamAddress;
|
||||||
|
Title = title;
|
||||||
|
TitleId = titleId;
|
||||||
|
Version = version;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsSelf { get; }
|
public bool IsSelf { get; }
|
||||||
@@ -63,4 +69,10 @@ public sealed class SelfImage
|
|||||||
public ulong EntryPoint => ElfHeader.EntryPoint + _imageBase;
|
public ulong EntryPoint => ElfHeader.EntryPoint + _imageBase;
|
||||||
|
|
||||||
public ulong ProcParamAddress { get; }
|
public ulong ProcParamAddress { get; }
|
||||||
|
|
||||||
|
public string? Title { get; }
|
||||||
|
|
||||||
|
public string? TitleId { get; }
|
||||||
|
|
||||||
|
public string? Version { get; }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -144,10 +144,9 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
throw new InvalidDataException("Input image is empty.");
|
throw new InvalidDataException("Input image is empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (readParamJson)
|
var applicationInfo = readParamJson
|
||||||
{
|
? TryLoadParamJson(fs, mountRoot)
|
||||||
TryLoadParamJson(fs, mountRoot);
|
: default;
|
||||||
}
|
|
||||||
|
|
||||||
if (clearVirtualMemory)
|
if (clearVirtualMemory)
|
||||||
{
|
{
|
||||||
@@ -270,15 +269,20 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
initializerFunctions,
|
initializerFunctions,
|
||||||
initFunctionEntryPoint,
|
initFunctionEntryPoint,
|
||||||
imageBase,
|
imageBase,
|
||||||
procParamAddress);
|
procParamAddress,
|
||||||
|
applicationInfo.Title,
|
||||||
|
applicationInfo.TitleId,
|
||||||
|
applicationInfo.Version);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TryLoadParamJson(IFileSystem? fs, string? mountRoot)
|
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
|
||||||
|
IFileSystem? fs,
|
||||||
|
string? mountRoot)
|
||||||
{
|
{
|
||||||
if (fs == null)
|
if (fs == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("[LOADER] param.json not found (no filesystem provided).");
|
Console.WriteLine("[LOADER] param.json not found (no filesystem provided).");
|
||||||
return;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
string[] possiblePaths = string.IsNullOrEmpty(mountRoot)
|
string[] possiblePaths = string.IsNullOrEmpty(mountRoot)
|
||||||
@@ -298,12 +302,16 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
if (foundPath == null)
|
if (foundPath == null)
|
||||||
{
|
{
|
||||||
Console.WriteLine("[LOADER] param.json not found (no root path / unknown layout).");
|
Console.WriteLine("[LOADER] param.json not found (no root path / unknown layout).");
|
||||||
return;
|
return default;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (title, titleId, ver) = Ps5ParamJsonReader.TryReadPs5Param(fs, foundPath);
|
var applicationInfo = Ps5ParamJsonReader.TryReadPs5Param(fs, foundPath);
|
||||||
Console.WriteLine($"[LOADER] Loading param.json at {foundPath}");
|
Console.WriteLine($"[LOADER] Loading param.json at {foundPath}");
|
||||||
Console.WriteLine($"[LOADER] Title: {title ?? "(unknown)"}, TitleId: {titleId ?? "(unknown)"}, Version: {ver ?? "(unknown)"}");
|
Console.WriteLine(
|
||||||
|
$"[LOADER] Title: {applicationInfo.Title ?? "(unknown)"}, " +
|
||||||
|
$"TitleId: {applicationInfo.TitleId ?? "(unknown)"}, " +
|
||||||
|
$"Version: {applicationInfo.Version ?? "(unknown)"}");
|
||||||
|
return applicationInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static LoadContext ParseLayout(ReadOnlySpan<byte> imageData)
|
private static LoadContext ParseLayout(ReadOnlySpan<byte> imageData)
|
||||||
|
|||||||
@@ -7,13 +7,17 @@ using SharpEmu.HLE;
|
|||||||
|
|
||||||
namespace SharpEmu.Core.Memory;
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
||||||
{
|
{
|
||||||
private readonly object _gate = new();
|
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||||
|
private readonly object _guestAllocationGate = new();
|
||||||
private readonly List<MemoryRegion> _regions = new();
|
private readonly List<MemoryRegion> _regions = new();
|
||||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private const ulong PageSize = 0x1000;
|
private const ulong PageSize = 0x1000;
|
||||||
|
private const ulong GuestAllocationArenaAddress = 0x00006000_0000_0000;
|
||||||
|
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
||||||
|
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
||||||
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
||||||
private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
|
private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
|
||||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||||
@@ -29,6 +33,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
private const uint PAGE_READWRITE = 0x04;
|
private const uint PAGE_READWRITE = 0x04;
|
||||||
private const uint PAGE_READONLY = 0x02;
|
private const uint PAGE_READONLY = 0x02;
|
||||||
|
|
||||||
|
private ulong _guestAllocationArenaBase;
|
||||||
|
private ulong _guestAllocationOffset;
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||||
|
|
||||||
@@ -71,7 +78,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_gate)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_regions.Add(new MemoryRegion
|
_regions.Add(new MemoryRegion
|
||||||
{
|
{
|
||||||
@@ -82,6 +90,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
Protection = protection
|
Protection = protection
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
var allocationKind = executable ? "executable memory" : "data memory";
|
var allocationKind = executable ? "executable memory" : "data memory";
|
||||||
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
||||||
@@ -195,7 +207,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_gate)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_regions.Add(new MemoryRegion
|
_regions.Add(new MemoryRegion
|
||||||
{
|
{
|
||||||
@@ -206,6 +219,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
Protection = protection
|
Protection = protection
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
var allocationKind = reservedOnly
|
var allocationKind = reservedOnly
|
||||||
? "reserved data memory (lazy commit)"
|
? "reserved data memory (lazy commit)"
|
||||||
@@ -215,16 +232,116 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
return actualAddress;
|
return actualAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public bool TryAllocateAtOrAbove(
|
||||||
|
ulong desiredAddress,
|
||||||
|
ulong size,
|
||||||
|
bool executable,
|
||||||
|
ulong alignment,
|
||||||
|
out ulong actualAddress)
|
||||||
|
{
|
||||||
|
actualAddress = 0;
|
||||||
|
if (size == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alignedSize = AlignUp(size, PageSize);
|
||||||
|
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
|
||||||
|
var cursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||||
|
|
||||||
|
for (var attempt = 0; attempt < 0x10000; attempt++)
|
||||||
|
{
|
||||||
|
if (cursor == 0 || ulong.MaxValue - cursor < alignedSize)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryGetOverlappingRegionEnd(cursor, alignedSize, out var overlapEnd))
|
||||||
|
{
|
||||||
|
cursor = AlignUp(overlapEnd, effectiveAlignment);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
||||||
|
if (actualAddress == cursor)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
actualAddress = 0;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
cursor = AlignUp(cursor + effectiveAlignment, effectiveAlignment);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
|
||||||
|
{
|
||||||
|
address = 0;
|
||||||
|
if (size == 0 || alignment == 0 || (alignment & (alignment - 1)) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_guestAllocationGate)
|
||||||
|
{
|
||||||
|
if (_guestAllocationArenaBase == 0)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_guestAllocationArenaBase = AllocateAt(
|
||||||
|
GuestAllocationArenaAddress,
|
||||||
|
GuestAllocationArenaSize,
|
||||||
|
executable: false,
|
||||||
|
allowAlternative: true);
|
||||||
|
_guestAllocationOffset = GuestAllocationArenaStartOffset;
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var alignedOffset = AlignUp(_guestAllocationOffset, alignment);
|
||||||
|
if (alignedOffset > GuestAllocationArenaSize || size > GuestAllocationArenaSize - alignedOffset)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
address = _guestAllocationArenaBase + alignedOffset;
|
||||||
|
_guestAllocationOffset = alignedOffset + size;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public void Clear()
|
public void Clear()
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_guestAllocationGate)
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
VirtualFree((void*)region.VirtualAddress, 0, MEM_RELEASE);
|
||||||
|
}
|
||||||
|
_regions.Clear();
|
||||||
|
_pageProtections.Clear();
|
||||||
}
|
}
|
||||||
_regions.Clear();
|
finally
|
||||||
_pageProtections.Clear();
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
_guestAllocationArenaBase = 0;
|
||||||
|
_guestAllocationOffset = 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -241,7 +358,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
var mapEnd = AlignUp(segmentEnd, PageSize);
|
var mapEnd = AlignUp(segmentEnd, PageSize);
|
||||||
var mapSize = checked(mapEnd - mapStart);
|
var mapSize = checked(mapEnd - mapStart);
|
||||||
|
|
||||||
lock (_gate)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var existingRegion = FindRegion(mapStart, mapSize);
|
var existingRegion = FindRegion(mapStart, mapSize);
|
||||||
if (existingRegion == null)
|
if (existingRegion == null)
|
||||||
@@ -274,6 +392,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
|
|
||||||
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
||||||
@@ -323,7 +445,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
|
|
||||||
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
||||||
{
|
{
|
||||||
lock (_gate)
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
||||||
for (var i = 0; i < _regions.Count; i++)
|
for (var i = 0; i < _regions.Count; i++)
|
||||||
@@ -338,11 +461,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
}
|
}
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
var requiresExclusiveAccess = false;
|
||||||
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
@@ -354,38 +483,55 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
if (region.IsReservedOnly)
|
||||||
{
|
{
|
||||||
return false;
|
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
fixed (byte* destPtr = destination)
|
|
||||||
{
|
{
|
||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
{
|
{
|
||||||
RestorePageProtections(touchedPages);
|
requiresExclusiveAccess = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
fixed (byte* destPtr = destination)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requiresExclusiveAccess)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return TryReadExclusive(virtualAddress, destination);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
var requiresExclusiveAccess = false;
|
||||||
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
@@ -397,37 +543,148 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
if (region.IsReservedOnly)
|
||||||
{
|
{
|
||||||
return false;
|
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||||
}
|
|
||||||
|
|
||||||
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
fixed (byte* srcPtr = source)
|
|
||||||
{
|
{
|
||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
return false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
|
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||||
{
|
{
|
||||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
requiresExclusiveAccess = true;
|
||||||
if (IsExecutableProtection(oldProtect))
|
break;
|
||||||
{
|
}
|
||||||
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
|
||||||
}
|
fixed (byte* srcPtr = source)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requiresExclusiveAccess)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return TryWriteExclusive(virtualAddress, source);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||||
|
{
|
||||||
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
if (!TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
|
{
|
||||||
|
fixed (byte* destPtr = destination)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fixed (byte* destPtr = destination)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
RestorePageProtections(touchedPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
|
{
|
||||||
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
if (!TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||||
|
{
|
||||||
|
fixed (byte* srcPtr = source)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fixed (byte* srcPtr = source)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||||
|
if (IsExecutableProtection(oldProtect))
|
||||||
|
{
|
||||||
|
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
|
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
|
||||||
@@ -439,7 +696,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
|
|
||||||
public void* GetPointer(ulong virtualAddress)
|
public void* GetPointer(ulong virtualAddress)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
@@ -451,11 +709,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsAccessible(ulong virtualAddress, ulong size)
|
public bool IsAccessible(ulong virtualAddress, ulong size)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
@@ -466,6 +729,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
}
|
}
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MemoryRegion? FindRegion(ulong address, ulong size)
|
private MemoryRegion? FindRegion(ulong address, ulong size)
|
||||||
@@ -480,6 +747,35 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool TryGetOverlappingRegionEnd(ulong address, ulong size, out ulong overlapEnd)
|
||||||
|
{
|
||||||
|
overlapEnd = 0;
|
||||||
|
if (size == 0 || ulong.MaxValue - address < size - 1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var end = address + size;
|
||||||
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
var regionEnd = region.VirtualAddress + region.Size;
|
||||||
|
if (address < regionEnd && region.VirtualAddress < end)
|
||||||
|
{
|
||||||
|
overlapEnd = Math.Max(overlapEnd, regionEnd);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
return overlapEnd != 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
|
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
|
||||||
{
|
{
|
||||||
offset = 0;
|
offset = 0;
|
||||||
@@ -507,6 +803,44 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
return protection is PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY;
|
return protection is PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanReadWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
|
||||||
|
CanAccessWithoutProtectionChange(address, size, region, write: false);
|
||||||
|
|
||||||
|
private bool CanWriteWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
|
||||||
|
CanAccessWithoutProtectionChange(address, size, region, write: true);
|
||||||
|
|
||||||
|
private bool CanAccessWithoutProtectionChange(ulong address, ulong size, MemoryRegion region, bool write)
|
||||||
|
{
|
||||||
|
var startPage = AlignDown(address, PageSize);
|
||||||
|
var endPage = AlignUp(address + size, PageSize);
|
||||||
|
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
||||||
|
{
|
||||||
|
if (_pageProtections.TryGetValue(pageAddress, out var flags))
|
||||||
|
{
|
||||||
|
if (write ? (flags & ProgramHeaderFlags.Write) == 0 : (flags & ProgramHeaderFlags.Read) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (write ? !IsWritableProtection(region.Protection) : !IsReadableProtection(region.Protection))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsReadableProtection(uint protection)
|
||||||
|
{
|
||||||
|
return protection is PAGE_READONLY or PAGE_READWRITE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWritableProtection(uint protection)
|
||||||
|
{
|
||||||
|
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
|
||||||
|
}
|
||||||
|
|
||||||
private static uint GetCommitProtection(MemoryRegion region)
|
private static uint GetCommitProtection(MemoryRegion region)
|
||||||
{
|
{
|
||||||
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
@@ -523,15 +857,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
var endPage = AlignUp(address + size, PageSize);
|
var endPage = AlignUp(address + size, PageSize);
|
||||||
var commitProtection = GetCommitProtection(region);
|
var commitProtection = GetCommitProtection(region);
|
||||||
|
|
||||||
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
var pageAddress = startPage;
|
||||||
|
while (pageAddress < endPage)
|
||||||
{
|
{
|
||||||
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
|
||||||
|
? ulong.MaxValue
|
||||||
|
: info.BaseAddress + info.RegionSize;
|
||||||
|
var rangeEnd = Math.Min(endPage, queriedEnd);
|
||||||
|
if (rangeEnd <= pageAddress)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (info.State == MEM_COMMIT)
|
if (info.State == MEM_COMMIT)
|
||||||
{
|
{
|
||||||
|
pageAddress = rangeEnd;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -540,10 +885,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VirtualAlloc((void*)pageAddress, (nuint)PageSize, MEM_COMMIT, commitProtection) == null)
|
var commitSize = rangeEnd - pageAddress;
|
||||||
|
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pageAddress = rangeEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ using SharpEmu.HLE;
|
|||||||
using SharpEmu.Libs.VideoOut;
|
using SharpEmu.Libs.VideoOut;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.AppContent;
|
using SharpEmu.Libs.AppContent;
|
||||||
|
using SharpEmu.Libs.SaveData;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
@@ -135,6 +136,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
LastMilestoneLog = null;
|
LastMilestoneLog = null;
|
||||||
KernelModuleRegistry.Reset();
|
KernelModuleRegistry.Reset();
|
||||||
var image = LoadImage(normalizedEbootPath);
|
var image = LoadImage(normalizedEbootPath);
|
||||||
|
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||||
|
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||||
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Diagnostics;
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
public readonly record struct GuestThreadStartRequest(
|
public readonly record struct GuestThreadStartRequest(
|
||||||
@@ -8,46 +10,159 @@ public readonly record struct GuestThreadStartRequest(
|
|||||||
ulong EntryPoint,
|
ulong EntryPoint,
|
||||||
ulong Argument,
|
ulong Argument,
|
||||||
ulong AttributeAddress,
|
ulong AttributeAddress,
|
||||||
string Name);
|
string Name,
|
||||||
|
int Priority,
|
||||||
|
ulong AffinityMask);
|
||||||
|
|
||||||
|
public readonly record struct GuestThreadSnapshot(
|
||||||
|
ulong ThreadHandle,
|
||||||
|
string Name,
|
||||||
|
string State,
|
||||||
|
long ImportCount,
|
||||||
|
string? LastImportNid,
|
||||||
|
ulong LastReturnRip,
|
||||||
|
string? BlockReason);
|
||||||
|
|
||||||
public interface IGuestThreadScheduler
|
public interface IGuestThreadScheduler
|
||||||
{
|
{
|
||||||
|
bool SupportsGuestContextTransfer { get; }
|
||||||
|
|
||||||
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
||||||
|
|
||||||
void Pump(CpuContext callerContext, string reason);
|
void Pump(CpuContext callerContext, string reason);
|
||||||
|
|
||||||
|
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
|
||||||
|
|
||||||
|
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
|
||||||
|
|
||||||
|
bool TryCallGuestFunction(
|
||||||
|
CpuContext callerContext,
|
||||||
|
ulong entryPoint,
|
||||||
|
ulong arg0,
|
||||||
|
ulong arg1,
|
||||||
|
ulong stackAddress,
|
||||||
|
ulong stackSize,
|
||||||
|
string reason,
|
||||||
|
out string? error);
|
||||||
|
|
||||||
|
bool TryCallGuestContinuation(
|
||||||
|
CpuContext callerContext,
|
||||||
|
GuestCpuContinuation continuation,
|
||||||
|
string reason,
|
||||||
|
out string? error);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public readonly record struct GuestImportCallFrame(
|
||||||
|
bool IsValid,
|
||||||
|
ulong ReturnRip,
|
||||||
|
ulong ResumeRsp,
|
||||||
|
ulong ReturnSlotAddress);
|
||||||
|
|
||||||
|
public readonly record struct GuestCpuContinuation(
|
||||||
|
ulong Rip,
|
||||||
|
ulong Rsp,
|
||||||
|
ulong ReturnSlotAddress,
|
||||||
|
ulong Rflags,
|
||||||
|
ulong FsBase,
|
||||||
|
ulong GsBase,
|
||||||
|
ulong Rax,
|
||||||
|
ulong Rcx,
|
||||||
|
ulong Rdx,
|
||||||
|
ulong Rbx,
|
||||||
|
ulong Rbp,
|
||||||
|
ulong Rsi,
|
||||||
|
ulong Rdi,
|
||||||
|
ulong R8,
|
||||||
|
ulong R9,
|
||||||
|
ulong R12,
|
||||||
|
ulong R13,
|
||||||
|
ulong R14,
|
||||||
|
ulong R15);
|
||||||
|
|
||||||
public static class GuestThreadExecution
|
public static class GuestThreadExecution
|
||||||
{
|
{
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static ulong _currentGuestThreadHandle;
|
private static ulong _currentGuestThreadHandle;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static ulong _currentFiberAddress;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static string? _pendingBlockReason;
|
private static string? _pendingBlockReason;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static bool _pendingBlockContinuationValid;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static GuestCpuContinuation _pendingBlockContinuation;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static string? _pendingBlockWakeKey;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static Func<int>? _pendingBlockResumeHandler;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static Func<bool>? _pendingBlockWakeHandler;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static long _pendingBlockDeadlineTimestamp;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static bool _pendingEntryExit;
|
private static bool _pendingEntryExit;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static int _pendingEntryExitStatus;
|
private static ulong _pendingEntryExitValue;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static string? _pendingEntryExitReason;
|
private static string? _pendingEntryExitReason;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static bool _pendingContextTransfer;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static GuestCpuContinuation _pendingContextTransferTarget;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static bool _hasCurrentImportCallFrame;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static ulong _currentImportReturnRip;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static ulong _currentImportResumeRsp;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static ulong _currentImportReturnSlotAddress;
|
||||||
|
|
||||||
public static IGuestThreadScheduler? Scheduler { get; set; }
|
public static IGuestThreadScheduler? Scheduler { get; set; }
|
||||||
|
|
||||||
public static bool IsGuestThread => _currentGuestThreadHandle != 0;
|
public static bool IsGuestThread => _currentGuestThreadHandle != 0;
|
||||||
|
|
||||||
public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle;
|
public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle;
|
||||||
|
|
||||||
|
public static ulong CurrentFiberAddress => _currentFiberAddress;
|
||||||
|
|
||||||
public static ulong EnterGuestThread(ulong threadHandle)
|
public static ulong EnterGuestThread(ulong threadHandle)
|
||||||
{
|
{
|
||||||
var previous = _currentGuestThreadHandle;
|
var previous = _currentGuestThreadHandle;
|
||||||
_currentGuestThreadHandle = threadHandle;
|
_currentGuestThreadHandle = threadHandle;
|
||||||
_pendingBlockReason = null;
|
_pendingBlockReason = null;
|
||||||
|
_pendingBlockContinuationValid = false;
|
||||||
|
_pendingBlockContinuation = default;
|
||||||
|
_pendingBlockWakeKey = null;
|
||||||
|
_pendingBlockResumeHandler = null;
|
||||||
|
_pendingBlockWakeHandler = null;
|
||||||
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitStatus = 0;
|
_pendingEntryExitValue = 0;
|
||||||
_pendingEntryExitReason = null;
|
_pendingEntryExitReason = null;
|
||||||
|
_pendingContextTransfer = false;
|
||||||
|
_pendingContextTransferTarget = default;
|
||||||
|
_hasCurrentImportCallFrame = false;
|
||||||
|
_currentImportReturnRip = 0;
|
||||||
|
_currentImportResumeRsp = 0;
|
||||||
|
_currentImportReturnSlotAddress = 0;
|
||||||
return previous;
|
return previous;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -55,12 +170,44 @@ public static class GuestThreadExecution
|
|||||||
{
|
{
|
||||||
_currentGuestThreadHandle = previousThreadHandle;
|
_currentGuestThreadHandle = previousThreadHandle;
|
||||||
_pendingBlockReason = null;
|
_pendingBlockReason = null;
|
||||||
|
_pendingBlockContinuationValid = false;
|
||||||
|
_pendingBlockContinuation = default;
|
||||||
|
_pendingBlockWakeKey = null;
|
||||||
|
_pendingBlockResumeHandler = null;
|
||||||
|
_pendingBlockWakeHandler = null;
|
||||||
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitStatus = 0;
|
_pendingEntryExitValue = 0;
|
||||||
_pendingEntryExitReason = null;
|
_pendingEntryExitReason = null;
|
||||||
|
_pendingContextTransfer = false;
|
||||||
|
_pendingContextTransferTarget = default;
|
||||||
|
_hasCurrentImportCallFrame = false;
|
||||||
|
_currentImportReturnRip = 0;
|
||||||
|
_currentImportResumeRsp = 0;
|
||||||
|
_currentImportReturnSlotAddress = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool RequestCurrentThreadBlock(string reason)
|
public static ulong EnterFiber(ulong fiberAddress)
|
||||||
|
{
|
||||||
|
var previous = _currentFiberAddress;
|
||||||
|
_currentFiberAddress = fiberAddress;
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RestoreFiber(ulong previousFiberAddress)
|
||||||
|
{
|
||||||
|
_currentFiberAddress = previousFiberAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool RequestCurrentThreadBlock(string reason) => RequestCurrentThreadBlock(null, reason);
|
||||||
|
|
||||||
|
public static bool RequestCurrentThreadBlock(
|
||||||
|
CpuContext? context,
|
||||||
|
string reason,
|
||||||
|
string? wakeKey = null,
|
||||||
|
Func<int>? resumeHandler = null,
|
||||||
|
Func<bool>? wakeHandler = null,
|
||||||
|
long blockDeadlineTimestamp = 0)
|
||||||
{
|
{
|
||||||
if (!IsGuestThread)
|
if (!IsGuestThread)
|
||||||
{
|
{
|
||||||
@@ -68,31 +215,167 @@ public static class GuestThreadExecution
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
||||||
|
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
||||||
|
_pendingBlockResumeHandler = resumeHandler;
|
||||||
|
_pendingBlockWakeHandler = wakeHandler;
|
||||||
|
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||||
|
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
||||||
|
{
|
||||||
|
_pendingBlockContinuation = continuation;
|
||||||
|
_pendingBlockContinuationValid = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pendingBlockContinuation = default;
|
||||||
|
_pendingBlockContinuationValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryConsumeCurrentThreadBlock(out string reason)
|
public static bool TryConsumeCurrentThreadBlock(out string reason)
|
||||||
|
{
|
||||||
|
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryConsumeCurrentThreadBlock(
|
||||||
|
out string reason,
|
||||||
|
out GuestCpuContinuation continuation,
|
||||||
|
out bool hasContinuation)
|
||||||
|
{
|
||||||
|
return TryConsumeCurrentThreadBlock(
|
||||||
|
out reason,
|
||||||
|
out continuation,
|
||||||
|
out hasContinuation,
|
||||||
|
out _,
|
||||||
|
out _,
|
||||||
|
out _,
|
||||||
|
out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryConsumeCurrentThreadBlock(
|
||||||
|
out string reason,
|
||||||
|
out GuestCpuContinuation continuation,
|
||||||
|
out bool hasContinuation,
|
||||||
|
out string wakeKey,
|
||||||
|
out Func<int>? resumeHandler,
|
||||||
|
out Func<bool>? wakeHandler)
|
||||||
|
{
|
||||||
|
return TryConsumeCurrentThreadBlock(
|
||||||
|
out reason,
|
||||||
|
out continuation,
|
||||||
|
out hasContinuation,
|
||||||
|
out wakeKey,
|
||||||
|
out resumeHandler,
|
||||||
|
out wakeHandler,
|
||||||
|
out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryConsumeCurrentThreadBlock(
|
||||||
|
out string reason,
|
||||||
|
out GuestCpuContinuation continuation,
|
||||||
|
out bool hasContinuation,
|
||||||
|
out string wakeKey,
|
||||||
|
out Func<int>? resumeHandler,
|
||||||
|
out Func<bool>? wakeHandler,
|
||||||
|
out long blockDeadlineTimestamp)
|
||||||
{
|
{
|
||||||
reason = _pendingBlockReason ?? string.Empty;
|
reason = _pendingBlockReason ?? string.Empty;
|
||||||
if (string.IsNullOrEmpty(reason))
|
if (string.IsNullOrEmpty(reason))
|
||||||
{
|
{
|
||||||
|
continuation = default;
|
||||||
|
hasContinuation = false;
|
||||||
|
wakeKey = string.Empty;
|
||||||
|
resumeHandler = null;
|
||||||
|
wakeHandler = null;
|
||||||
|
blockDeadlineTimestamp = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
continuation = _pendingBlockContinuation;
|
||||||
|
hasContinuation = _pendingBlockContinuationValid;
|
||||||
|
wakeKey = _pendingBlockWakeKey ?? reason;
|
||||||
|
resumeHandler = _pendingBlockResumeHandler;
|
||||||
|
wakeHandler = _pendingBlockWakeHandler;
|
||||||
|
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
||||||
_pendingBlockReason = null;
|
_pendingBlockReason = null;
|
||||||
|
_pendingBlockContinuation = default;
|
||||||
|
_pendingBlockContinuationValid = false;
|
||||||
|
_pendingBlockWakeKey = null;
|
||||||
|
_pendingBlockResumeHandler = null;
|
||||||
|
_pendingBlockWakeHandler = null;
|
||||||
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
|
||||||
|
{
|
||||||
|
if (timeout <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
return Stopwatch.GetTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
var ticks = timeout.TotalSeconds >= long.MaxValue / (double)Stopwatch.Frequency
|
||||||
|
? long.MaxValue
|
||||||
|
: (long)Math.Ceiling(timeout.TotalSeconds * Stopwatch.Frequency);
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
if (long.MaxValue - now <= ticks)
|
||||||
|
{
|
||||||
|
return long.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return now + Math.Max(1, ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCaptureCurrentBlockContinuation(CpuContext context, out GuestCpuContinuation continuation)
|
||||||
|
{
|
||||||
|
if (!TryGetCurrentImportCallFrame(out var frame) ||
|
||||||
|
frame.ReturnRip < 65536 ||
|
||||||
|
frame.ResumeRsp == 0 ||
|
||||||
|
frame.ReturnSlotAddress == 0)
|
||||||
|
{
|
||||||
|
continuation = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation = new GuestCpuContinuation(
|
||||||
|
frame.ReturnRip,
|
||||||
|
frame.ResumeRsp,
|
||||||
|
frame.ReturnSlotAddress,
|
||||||
|
context.Rflags,
|
||||||
|
context.FsBase,
|
||||||
|
context.GsBase,
|
||||||
|
0,
|
||||||
|
context[CpuRegister.Rcx],
|
||||||
|
context[CpuRegister.Rdx],
|
||||||
|
context[CpuRegister.Rbx],
|
||||||
|
context[CpuRegister.Rbp],
|
||||||
|
context[CpuRegister.Rsi],
|
||||||
|
context[CpuRegister.Rdi],
|
||||||
|
context[CpuRegister.R8],
|
||||||
|
context[CpuRegister.R9],
|
||||||
|
context[CpuRegister.R12],
|
||||||
|
context[CpuRegister.R13],
|
||||||
|
context[CpuRegister.R14],
|
||||||
|
context[CpuRegister.R15]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RequestCurrentEntryExit(string reason, int status)
|
public static void RequestCurrentEntryExit(string reason, int status)
|
||||||
|
{
|
||||||
|
RequestCurrentEntryExit(reason, unchecked((ulong)(long)status));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RequestCurrentEntryExit(string reason, ulong value)
|
||||||
{
|
{
|
||||||
_pendingEntryExit = true;
|
_pendingEntryExit = true;
|
||||||
_pendingEntryExitStatus = status;
|
_pendingEntryExitValue = value;
|
||||||
_pendingEntryExitReason = string.IsNullOrWhiteSpace(reason) ? "guest_entry_exit" : reason;
|
_pendingEntryExitReason = string.IsNullOrWhiteSpace(reason) ? "guest_entry_exit" : reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryConsumeCurrentEntryExit(out int status, out string reason)
|
public static bool TryConsumeCurrentEntryExit(out ulong value, out string reason)
|
||||||
{
|
{
|
||||||
status = _pendingEntryExitStatus;
|
value = _pendingEntryExitValue;
|
||||||
reason = _pendingEntryExitReason ?? string.Empty;
|
reason = _pendingEntryExitReason ?? string.Empty;
|
||||||
if (!_pendingEntryExit)
|
if (!_pendingEntryExit)
|
||||||
{
|
{
|
||||||
@@ -100,8 +383,68 @@ public static class GuestThreadExecution
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitStatus = 0;
|
_pendingEntryExitValue = 0;
|
||||||
_pendingEntryExitReason = null;
|
_pendingEntryExitReason = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void RequestCurrentContextTransfer(GuestCpuContinuation target)
|
||||||
|
{
|
||||||
|
_pendingContextTransferTarget = target;
|
||||||
|
_pendingContextTransfer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryConsumeCurrentContextTransfer(out GuestCpuContinuation target)
|
||||||
|
{
|
||||||
|
target = _pendingContextTransferTarget;
|
||||||
|
if (!_pendingContextTransfer)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingContextTransfer = false;
|
||||||
|
_pendingContextTransferTarget = default;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static GuestImportCallFrame EnterImportCallFrame(
|
||||||
|
ulong returnRip,
|
||||||
|
ulong resumeRsp,
|
||||||
|
ulong returnSlotAddress)
|
||||||
|
{
|
||||||
|
var previous = new GuestImportCallFrame(
|
||||||
|
_hasCurrentImportCallFrame,
|
||||||
|
_currentImportReturnRip,
|
||||||
|
_currentImportResumeRsp,
|
||||||
|
_currentImportReturnSlotAddress);
|
||||||
|
_hasCurrentImportCallFrame = true;
|
||||||
|
_currentImportReturnRip = returnRip;
|
||||||
|
_currentImportResumeRsp = resumeRsp;
|
||||||
|
_currentImportReturnSlotAddress = returnSlotAddress;
|
||||||
|
return previous;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RestoreImportCallFrame(GuestImportCallFrame previous)
|
||||||
|
{
|
||||||
|
_hasCurrentImportCallFrame = previous.IsValid;
|
||||||
|
_currentImportReturnRip = previous.ReturnRip;
|
||||||
|
_currentImportResumeRsp = previous.ResumeRsp;
|
||||||
|
_currentImportReturnSlotAddress = previous.ReturnSlotAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryGetCurrentImportCallFrame(out GuestImportCallFrame frame)
|
||||||
|
{
|
||||||
|
if (!_hasCurrentImportCallFrame)
|
||||||
|
{
|
||||||
|
frame = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame = new GuestImportCallFrame(
|
||||||
|
true,
|
||||||
|
_currentImportReturnRip,
|
||||||
|
_currentImportResumeRsp,
|
||||||
|
_currentImportReturnSlotAddress);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
|
public interface IGuestMemoryAllocator
|
||||||
|
{
|
||||||
|
bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address);
|
||||||
|
}
|
||||||
@@ -2,9 +2,10 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.VideoOut;
|
using SharpEmu.Libs.VideoOut;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Threading;
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
@@ -16,7 +17,9 @@ public static class AgcExports
|
|||||||
private const uint ItIndexBufferSize = 0x13;
|
private const uint ItIndexBufferSize = 0x13;
|
||||||
private const uint ItIndexBase = 0x26;
|
private const uint ItIndexBase = 0x26;
|
||||||
private const uint ItIndexType = 0x2A;
|
private const uint ItIndexType = 0x2A;
|
||||||
|
private const uint ItNumInstances = 0x2F;
|
||||||
private const uint ItDrawIndexOffset2 = 0x35;
|
private const uint ItDrawIndexOffset2 = 0x35;
|
||||||
|
private const uint ItEventWrite = 0x46;
|
||||||
private const uint ItSetShReg = 0x76;
|
private const uint ItSetShReg = 0x76;
|
||||||
private const uint RZero = 0x00;
|
private const uint RZero = 0x00;
|
||||||
private const uint RDrawIndexAuto = 0x04;
|
private const uint RDrawIndexAuto = 0x04;
|
||||||
@@ -25,11 +28,19 @@ public static class AgcExports
|
|||||||
private const uint RShRegsIndirect = 0x11;
|
private const uint RShRegsIndirect = 0x11;
|
||||||
private const uint RCxRegsIndirect = 0x12;
|
private const uint RCxRegsIndirect = 0x12;
|
||||||
private const uint RUcRegsIndirect = 0x13;
|
private const uint RUcRegsIndirect = 0x13;
|
||||||
|
private const uint RAcquireMem = 0x14;
|
||||||
private const uint RFlip = 0x17;
|
private const uint RFlip = 0x17;
|
||||||
|
private const uint RReleaseMem = 0x18;
|
||||||
private const uint SpiShaderPgmLoPs = 0x8;
|
private const uint SpiShaderPgmLoPs = 0x8;
|
||||||
private const uint SpiShaderPgmHiPs = 0x9;
|
private const uint SpiShaderPgmHiPs = 0x9;
|
||||||
private const uint SpiShaderPgmLoEs = 0xC8;
|
private const uint SpiShaderPgmLoEs = 0xC8;
|
||||||
private const uint SpiShaderPgmHiEs = 0xC9;
|
private const uint SpiShaderPgmHiEs = 0xC9;
|
||||||
|
private const uint SpiShaderPgmLoLs = 0x148;
|
||||||
|
private const uint SpiShaderPgmHiLs = 0x149;
|
||||||
|
private const uint SpiPsInputEna = 0x1B3;
|
||||||
|
private const uint SpiPsInputAddr = 0x1B4;
|
||||||
|
private const uint ComputePgmLo = 0x20C;
|
||||||
|
private const uint ComputePgmHi = 0x20D;
|
||||||
private const uint SpiPsInputCntl0 = 0x191;
|
private const uint SpiPsInputCntl0 = 0x191;
|
||||||
private const uint VgtPrimitiveType = 0x242;
|
private const uint VgtPrimitiveType = 0x242;
|
||||||
private const uint PsTextureUserDataRegister = 0xC;
|
private const uint PsTextureUserDataRegister = 0xC;
|
||||||
@@ -37,6 +48,13 @@ public static class AgcExports
|
|||||||
private const uint Gen5TextureType2D = 9;
|
private const uint Gen5TextureType2D = 9;
|
||||||
private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
|
private const ulong VideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
|
||||||
private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
|
private const ulong VideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
|
||||||
|
private const ulong VideoOutPixelFormatB8G8R8A8Unorm = 0x8100000000000000;
|
||||||
|
private const ulong VideoOutPixelFormatR8G8B8A8Unorm = 0x8100000022000000;
|
||||||
|
private const uint RegisterDefaultsVersion7 = 7;
|
||||||
|
private const uint RegisterDefaultsVersion8 = 8;
|
||||||
|
private const uint RegisterDefaultsVersion10 = 10;
|
||||||
|
private const int RegisterDefaultsSize = 0x40;
|
||||||
|
private const int RegisterDefaultBlockSize = 16 * 8;
|
||||||
|
|
||||||
private const ulong ShaderUserDataOffset = 0x08;
|
private const ulong ShaderUserDataOffset = 0x08;
|
||||||
private const ulong ShaderCodeOffset = 0x10;
|
private const ulong ShaderCodeOffset = 0x10;
|
||||||
@@ -58,9 +76,86 @@ public static class AgcExports
|
|||||||
private const ulong ShaderSpecialVgtGsOutPrimTypeOffset = 0x20;
|
private const ulong ShaderSpecialVgtGsOutPrimTypeOffset = 0x20;
|
||||||
private const ulong ShaderSpecialGeUserVgprEnOffset = 0x28;
|
private const ulong ShaderSpecialGeUserVgprEnOffset = 0x28;
|
||||||
private const uint CbSetShRegisterRangeMarker = 0x6875000D;
|
private const uint CbSetShRegisterRangeMarker = 0x6875000D;
|
||||||
|
private static readonly object _submitTraceGate = new();
|
||||||
|
private static readonly HashSet<uint> _tracedDcbSizes = new();
|
||||||
|
private static readonly HashSet<(ulong Es, ulong Ps, GuestDrawKind Kind)> _tracedShaderTranslations = new();
|
||||||
private static readonly object _softwarePresenterGate = new();
|
private static readonly object _softwarePresenterGate = new();
|
||||||
private static readonly Dictionary<(ulong Source, ulong Destination), ulong> _softwarePresenterFingerprints = new();
|
private static readonly Dictionary<(ulong Source, ulong Destination), ulong> _softwarePresenterFingerprints = new();
|
||||||
private static int _submitTraceCaptured;
|
private static readonly object _registerDefaultsGate = new();
|
||||||
|
private static readonly ConditionalWeakTable<object, RegisterDefaultsAllocation> _registerDefaultsAllocations = new();
|
||||||
|
|
||||||
|
private static readonly RegisterDefaultGroup[] PrimaryRegisterDefaults =
|
||||||
|
[
|
||||||
|
new(0, 3, 0x0BC65DA4, [new(0x08F, 0)]),
|
||||||
|
new(0, 4, 0x9E5AD592, [new(0x08E, 0)]),
|
||||||
|
new(0, 12, 0x6DE4C312, [new(0x203, 0)]),
|
||||||
|
new(0, 72, 0x38E92C91,
|
||||||
|
[
|
||||||
|
new(0x318, 0),
|
||||||
|
new(0x31B, 0),
|
||||||
|
new(0x31C, 0),
|
||||||
|
new(0x31D, 0),
|
||||||
|
new(0x31E, 0x48),
|
||||||
|
new(0x31F, 0),
|
||||||
|
new(0x321, 0),
|
||||||
|
new(0x323, 0),
|
||||||
|
new(0x324, 0),
|
||||||
|
new(0x325, 0),
|
||||||
|
new(0x390, 0),
|
||||||
|
new(0x398, 0),
|
||||||
|
new(0x3A0, 0),
|
||||||
|
new(0x3A8, 0),
|
||||||
|
new(0x3B0, 0),
|
||||||
|
new(0x3B8, 0x0006C000),
|
||||||
|
]),
|
||||||
|
new(0, 73, 0x0B177B43, [new(0x00C, 0), new(0x00D, 0x40004000)]),
|
||||||
|
new(0, 74, 0x48531062, [new(0x191, 0)]),
|
||||||
|
new(0, 76, 0x7690AF6F,
|
||||||
|
[
|
||||||
|
new(0x10F, 0x4E7E0000),
|
||||||
|
new(0x111, 0x4E7E0000),
|
||||||
|
new(0x113, 0x4E7E0000),
|
||||||
|
new(0x110, 0),
|
||||||
|
new(0x112, 0),
|
||||||
|
new(0x114, 0),
|
||||||
|
new(0x094, 0x80000000),
|
||||||
|
new(0x095, 0x40004000),
|
||||||
|
new(0x0B4, 0),
|
||||||
|
new(0x0B5, 0),
|
||||||
|
]),
|
||||||
|
new(1, 13, 0xC918DF3E, [new(0x20C, 0), new(0x20D, 0)]),
|
||||||
|
new(1, 14, 0xC9751C9C, [new(0x0C8, 0), new(0x0C9, 0)]),
|
||||||
|
new(1, 18, 0xC9E01B31, [new(0x008, 0), new(0x009, 0)]),
|
||||||
|
new(2, 3, 0x105971C2, [new(0x25B, 0)]),
|
||||||
|
new(2, 7, 0x40D49AD1, [new(0x262, 0)]),
|
||||||
|
new(2, 12, 0x9EBFAB10, [new(0x242, 0)]),
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly RegisterDefaultGroup[] InternalRegisterDefaults =
|
||||||
|
[
|
||||||
|
new(0, 0, 0x8FB4EDB5, [new(0x00E, 0)]),
|
||||||
|
new(0, 1, 0xB994AD29, [new(0x2AF, 0)]),
|
||||||
|
new(0, 2, 0xD427322F, [new(0x314, 0)]),
|
||||||
|
new(0, 3, 0xF58FEA31, [new(0x1B5, 0)]),
|
||||||
|
new(1, 0, 0x6AC156EF, [new(0x216, 0)]),
|
||||||
|
new(1, 1, 0x6AC15610, [new(0x217, 0)]),
|
||||||
|
new(1, 2, 0x6AC15009, [new(0x219, 0)]),
|
||||||
|
new(1, 3, 0x6AC153BA, [new(0x21A, 0)]),
|
||||||
|
new(1, 4, 0xBE7DCD73, [new(0x27D, 0)]),
|
||||||
|
new(1, 5, 0x0C4B1438, [new(0x22A, 0)]),
|
||||||
|
new(1, 6, 0xDB00D71A, [new(0x204, 0)]),
|
||||||
|
new(1, 7, 0xDB00D249, [new(0x205, 0)]),
|
||||||
|
new(1, 8, 0xDB00EC60, [new(0x206, 0)]),
|
||||||
|
new(1, 9, 0x0C4D6FE4, [new(0x080, 0)]),
|
||||||
|
new(1, 10, 0x0C4A80EF, [new(0x100, 0)]),
|
||||||
|
new(1, 11, 0x0DD283E7, [new(0x006, 0)]),
|
||||||
|
new(1, 12, 0xC620E68C, [new(0x081, 0)]),
|
||||||
|
new(1, 13, 0xC67EFACF, [new(0x101, 0)]),
|
||||||
|
new(1, 14, 0xD9E6D9F7, [new(0x001, 0)]),
|
||||||
|
new(2, 0, 0x31F34B9F, [new(0x24F, 0)]),
|
||||||
|
new(2, 1, 0xAC0F9E76, [new(0x80003FFF, 0)]),
|
||||||
|
new(2, 2, 0x929FD95D, [new(0x250, 0)]),
|
||||||
|
];
|
||||||
|
|
||||||
private readonly record struct TextureDescriptor(
|
private readonly record struct TextureDescriptor(
|
||||||
ulong Address,
|
ulong Address,
|
||||||
@@ -70,6 +165,57 @@ public static class AgcExports
|
|||||||
uint TileMode,
|
uint TileMode,
|
||||||
uint Type);
|
uint Type);
|
||||||
|
|
||||||
|
private sealed class SubmittedDcbState
|
||||||
|
{
|
||||||
|
public Dictionary<uint, uint> CxRegisters { get; } = new();
|
||||||
|
public Dictionary<uint, uint> ShRegisters { get; } = new();
|
||||||
|
public Dictionary<uint, uint> UcRegisters { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct RegisterDefaultValue(uint Offset, uint Value);
|
||||||
|
|
||||||
|
private readonly record struct RegisterDefaultGroup(
|
||||||
|
uint Space,
|
||||||
|
uint Index,
|
||||||
|
uint Type,
|
||||||
|
RegisterDefaultValue[] Registers);
|
||||||
|
|
||||||
|
private sealed record RegisterDefaultsAllocation(ulong Primary, ulong Internal);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "23LRUSvYu1M",
|
||||||
|
ExportName = "sceAgcInit",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int Init(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var stateAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var version = (uint)ctx[CpuRegister.Rsi];
|
||||||
|
if (stateAddress == 0 || !IsSupportedRegisterDefaultsVersion(version))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc($"agc.init state=0x{stateAddress:X16} version={version}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "2JtWUUiYBXs",
|
||||||
|
ExportName = "sceAgcGetRegisterDefaults2",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int GetRegisterDefaults2(CpuContext ctx) =>
|
||||||
|
ReturnRegisterDefaults(ctx, internalDefaults: false);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "wRbq6ZjNop4",
|
||||||
|
ExportName = "sceAgcGetRegisterDefaults2Internal",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int GetRegisterDefaults2Internal(CpuContext ctx) =>
|
||||||
|
ReturnRegisterDefaults(ctx, internalDefaults: true);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "f3dg2CSgRKY",
|
Nid = "f3dg2CSgRKY",
|
||||||
ExportName = "sceAgcCreateShader",
|
ExportName = "sceAgcCreateShader",
|
||||||
@@ -80,7 +226,7 @@ public static class AgcExports
|
|||||||
var destinationAddress = ctx[CpuRegister.Rdi];
|
var destinationAddress = ctx[CpuRegister.Rdi];
|
||||||
var headerAddress = ctx[CpuRegister.Rsi];
|
var headerAddress = ctx[CpuRegister.Rsi];
|
||||||
var codeAddress = ctx[CpuRegister.Rdx];
|
var codeAddress = ctx[CpuRegister.Rdx];
|
||||||
if (destinationAddress == 0 || headerAddress == 0 || codeAddress == 0)
|
if (headerAddress == 0 || codeAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
@@ -128,7 +274,8 @@ public static class AgcExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(destinationAddress, headerAddress))
|
if (destinationAddress != 0 &&
|
||||||
|
!ctx.TryWriteUInt64(destinationAddress, headerAddress))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
@@ -204,7 +351,7 @@ public static class AgcExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadByte(ctx, geometryShaderAddress + ShaderTypeOffset, out var shaderType) || shaderType != 2 ||
|
if (!TryReadByte(ctx, geometryShaderAddress + ShaderTypeOffset, out var shaderType) || !IsEsGeometryShaderType(shaderType) ||
|
||||||
!TryReadUInt64(ctx, geometryShaderAddress + ShaderSpecialsOffset, out var specialsAddress) ||
|
!TryReadUInt64(ctx, geometryShaderAddress + ShaderSpecialsOffset, out var specialsAddress) ||
|
||||||
specialsAddress == 0)
|
specialsAddress == 0)
|
||||||
{
|
{
|
||||||
@@ -221,7 +368,7 @@ public static class AgcExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.create_prim_state cx=0x{cxRegistersAddress:X16} uc=0x{ucRegistersAddress:X16} gs=0x{geometryShaderAddress:X16} prim=0x{primitiveType:X8}");
|
TraceAgc($"agc.create_prim_state cx=0x{cxRegistersAddress:X16} uc=0x{ucRegistersAddress:X16} gs=0x{geometryShaderAddress:X16} type={shaderType} prim=0x{primitiveType:X8}");
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -354,6 +501,64 @@ public static class AgcExports
|
|||||||
return ReturnPointer(ctx, commandAddress);
|
return ReturnPointer(ctx, commandAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "wr23dPKyWc0",
|
||||||
|
ExportName = "sceAgcCbReleaseMem",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int CbReleaseMem(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var action = (uint)(ctx[CpuRegister.Rsi] & 0xFF);
|
||||||
|
var gcrControl = (uint)(ctx[CpuRegister.Rdx] & 0xFFFF);
|
||||||
|
var destination = (uint)(ctx[CpuRegister.Rcx] & 0xFF);
|
||||||
|
var cachePolicy = (uint)(ctx[CpuRegister.R8] & 0xFF);
|
||||||
|
var destinationAddress = ctx[CpuRegister.R9];
|
||||||
|
var stackAddress = ctx[CpuRegister.Rsp];
|
||||||
|
if (!TryReadUInt64(ctx, stackAddress + 8, out var dataSelectionRaw) ||
|
||||||
|
!TryReadUInt64(ctx, stackAddress + 16, out var data) ||
|
||||||
|
!TryReadUInt64(ctx, stackAddress + 24, out var gdsOffsetRaw) ||
|
||||||
|
!TryReadUInt64(ctx, stackAddress + 32, out var gdsSizeRaw) ||
|
||||||
|
!TryReadUInt64(ctx, stackAddress + 40, out var interruptRaw) ||
|
||||||
|
!TryReadUInt64(ctx, stackAddress + 48, out var interruptContextIdRaw))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataSelection = (uint)(dataSelectionRaw & 0xFF);
|
||||||
|
var gdsOffset = (uint)(gdsOffsetRaw & 0xFFFF);
|
||||||
|
var gdsSize = (uint)(gdsSizeRaw & 0xFFFF);
|
||||||
|
var interrupt = (uint)(interruptRaw & 0xFF);
|
||||||
|
var interruptContextId = (uint)interruptContextIdRaw;
|
||||||
|
if (commandBufferAddress == 0 ||
|
||||||
|
destination != 1 ||
|
||||||
|
dataSelection is not (2 or 3) ||
|
||||||
|
gdsOffset != 0 ||
|
||||||
|
gdsSize != 1 ||
|
||||||
|
interrupt != 0 ||
|
||||||
|
interruptContextId != 0)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RReleaseMem)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 4, action | (cachePolicy << 8)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 8, gcrControl | (dataSelection << 16)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 12, (uint)destinationAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 16, (uint)(destinationAddress >> 32)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 20, (uint)data) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 24, (uint)(data >> 32)))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.cb_release_mem buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " +
|
||||||
|
$"action=0x{action:X2} gcr=0x{gcrControl:X4} dst=0x{destinationAddress:X16} data_sel={dataSelection} data=0x{data:X16}");
|
||||||
|
return ReturnPointer(ctx, commandAddress);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "TRO721eVt4g",
|
Nid = "TRO721eVt4g",
|
||||||
ExportName = "sceAgcDcbResetQueue",
|
ExportName = "sceAgcDcbResetQueue",
|
||||||
@@ -430,6 +635,136 @@ public static class AgcExports
|
|||||||
return ReturnPointer(ctx, commandAddress);
|
return ReturnPointer(ctx, commandAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "tSBxhAPyytQ",
|
||||||
|
ExportName = "sceAgcDcbSetNumInstances",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbSetNumInstances(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var instanceCount = (uint)ctx[CpuRegister.Rsi];
|
||||||
|
if (commandBufferAddress == 0)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItNumInstances, 0)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 4, instanceCount))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc($"agc.dcb_set_num_instances buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} count={instanceCount}");
|
||||||
|
return ReturnPointer(ctx, commandAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Yw0jKSqop+E",
|
||||||
|
ExportName = "sceAgcDcbDrawIndexAuto",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbDrawIndexAuto(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var indexCount = (uint)ctx[CpuRegister.Rsi];
|
||||||
|
var modifier = ctx[CpuRegister.Rdx];
|
||||||
|
if (commandBufferAddress == 0 || modifier != 0x4000_0000)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 7, out var commandAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress, Pm4(7, ItNop, RDrawIndexAuto)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 4, indexCount) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 8, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 16, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 20, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 24, 0))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc($"agc.dcb_draw_index_auto buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} count={indexCount}");
|
||||||
|
return ReturnPointer(ctx, commandAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "aJf+j5yntiU",
|
||||||
|
ExportName = "sceAgcDcbEventWrite",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbEventWrite(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var eventType = (uint)(ctx[CpuRegister.Rsi] & 0xFF);
|
||||||
|
var eventAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (commandBufferAddress == 0 || eventType > 0x3F || eventAddress != 0)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 2, out var commandAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress, Pm4(2, ItEventWrite, 0)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 4, eventType))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc($"agc.dcb_event_write buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} type={eventType}");
|
||||||
|
return ReturnPointer(ctx, commandAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "57labkp+rSQ",
|
||||||
|
ExportName = "sceAgcDcbAcquireMem",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int DcbAcquireMem(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBufferAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var engine = (uint)(ctx[CpuRegister.Rsi] & 0xFF);
|
||||||
|
var cbDbOp = (uint)ctx[CpuRegister.Rdx];
|
||||||
|
var gcrControl = (uint)ctx[CpuRegister.Rcx];
|
||||||
|
var baseAddress = ctx[CpuRegister.R8];
|
||||||
|
var sizeBytes = ctx[CpuRegister.R9];
|
||||||
|
if (!TryReadUInt32(ctx, ctx[CpuRegister.Rsp] + sizeof(ulong), out var pollCycles))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var noSize = sizeBytes == ulong.MaxValue;
|
||||||
|
if (commandBufferAddress == 0 ||
|
||||||
|
engine > 1 ||
|
||||||
|
(!noSize && (sizeBytes & 0xFF) != 0) ||
|
||||||
|
(!noSize && (sizeBytes >> 40) != 0) ||
|
||||||
|
(baseAddress & 0xFF) != 0 ||
|
||||||
|
(baseAddress >> 40) != 0)
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateCommandDwords(ctx, commandBufferAddress, 8, out var commandAddress) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress, Pm4(8, ItNop, RAcquireMem)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 4, (engine << 31) | cbDbOp) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 8, noSize ? 0 : (uint)(sizeBytes >> 8)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 12, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 16, (uint)(baseAddress >> 8)) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 20, 0) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 24, pollCycles / 40) ||
|
||||||
|
!TryWriteUInt32(ctx, commandAddress + 28, gcrControl))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb_acquire_mem buf=0x{commandBufferAddress:X16} cmd=0x{commandAddress:X16} " +
|
||||||
|
$"engine={engine} cbdb=0x{cbDbOp:X8} gcr=0x{gcrControl:X8} base=0x{baseAddress:X16} size=0x{sizeBytes:X16}");
|
||||||
|
return ReturnPointer(ctx, commandAddress);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "l4fM9K-Lyks",
|
Nid = "l4fM9K-Lyks",
|
||||||
ExportName = "sceAgcDcbSetIndexBuffer",
|
ExportName = "sceAgcDcbSetIndexBuffer",
|
||||||
@@ -552,6 +887,50 @@ public static class AgcExports
|
|||||||
return ReturnPointer(ctx, commandAddress);
|
return ReturnPointer(ctx, commandAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "w2rJhmD+dsE",
|
||||||
|
ExportName = "sceAgcDriverAddEqEvent",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgcDriver")]
|
||||||
|
public static int DriverAddEqEvent(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var equeue = ctx[CpuRegister.Rdi];
|
||||||
|
var eventId = ctx[CpuRegister.Rsi];
|
||||||
|
var userData = ctx[CpuRegister.Rdx];
|
||||||
|
if (!KernelEventQueueCompatExports.RegisterEvent(
|
||||||
|
equeue,
|
||||||
|
eventId,
|
||||||
|
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
||||||
|
userData))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc($"agc.driver_add_eq_event eq=0x{equeue:X16} id=0x{eventId:X16} udata=0x{userData:X16}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DL2RXaXOy88",
|
||||||
|
ExportName = "sceAgcDriverDeleteEqEvent",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgcDriver")]
|
||||||
|
public static int DriverDeleteEqEvent(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var equeue = ctx[CpuRegister.Rdi];
|
||||||
|
var eventId = ctx[CpuRegister.Rsi];
|
||||||
|
if (!KernelEventQueueCompatExports.DeleteRegisteredEvent(
|
||||||
|
equeue,
|
||||||
|
eventId,
|
||||||
|
KernelEventQueueCompatExports.KernelEventFilterGraphics))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAgc($"agc.driver_delete_eq_event eq=0x{equeue:X16} id=0x{eventId:X16}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "UglJIZjGssM",
|
Nid = "UglJIZjGssM",
|
||||||
ExportName = "sceAgcDriverSubmitDcb",
|
ExportName = "sceAgcDriverSubmitDcb",
|
||||||
@@ -567,10 +946,20 @@ public static class AgcExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}");
|
var tracePackets = false;
|
||||||
var tracePackets =
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal))
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) &&
|
{
|
||||||
Interlocked.CompareExchange(ref _submitTraceCaptured, 1, 0) == 0;
|
lock (_submitTraceGate)
|
||||||
|
{
|
||||||
|
tracePackets = _tracedDcbSizes.Add(dwordCount);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracePackets)
|
||||||
|
{
|
||||||
|
TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}");
|
||||||
|
}
|
||||||
|
|
||||||
ParseSubmittedDcb(ctx, commandAddress, dwordCount, tracePackets);
|
ParseSubmittedDcb(ctx, commandAddress, dwordCount, tracePackets);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -588,6 +977,20 @@ public static class AgcExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "qj7QZpgr9Uw",
|
||||||
|
ExportName = "sceAgcUnknownQj7QZpgr9Uw",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgc")]
|
||||||
|
public static int UnknownQj7QZpgr9Uw(CpuContext ctx)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.unknown_qj7 rdi=0x{ctx[CpuRegister.Rdi]:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} " +
|
||||||
|
$"rdx=0x{ctx[CpuRegister.Rdx]:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16}");
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
private static void ParseSubmittedDcb(CpuContext ctx, ulong commandAddress, uint dwordCount, bool tracePackets)
|
private static void ParseSubmittedDcb(CpuContext ctx, ulong commandAddress, uint dwordCount, bool tracePackets)
|
||||||
{
|
{
|
||||||
if (commandAddress == 0 || dwordCount == 0 || dwordCount > 1_000_000)
|
if (commandAddress == 0 || dwordCount == 0 || dwordCount > 1_000_000)
|
||||||
@@ -596,6 +999,8 @@ public static class AgcExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
TextureDescriptor? presenterTexture = null;
|
TextureDescriptor? presenterTexture = null;
|
||||||
|
GuestDrawKind guestDrawKind = GuestDrawKind.None;
|
||||||
|
var state = new SubmittedDcbState();
|
||||||
var sawIndexedDraw = false;
|
var sawIndexedDraw = false;
|
||||||
var offset = 0u;
|
var offset = 0u;
|
||||||
while (offset < dwordCount)
|
while (offset < dwordCount)
|
||||||
@@ -625,12 +1030,45 @@ public static class AgcExports
|
|||||||
presenterTexture = texture;
|
presenterTexture = texture;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ApplySubmittedRegisters(ctx, state, currentAddress, length, op, register);
|
||||||
|
|
||||||
|
if (op == ItEventWrite &&
|
||||||
|
length >= 2 &&
|
||||||
|
TryReadUInt32(ctx, currentAddress + sizeof(uint), out var eventTypeRaw))
|
||||||
|
{
|
||||||
|
var eventType = eventTypeRaw & 0x3Fu;
|
||||||
|
var triggered = KernelEventQueueCompatExports.TriggerRegisteredEvents(
|
||||||
|
eventType,
|
||||||
|
KernelEventQueueCompatExports.KernelEventFilterGraphics,
|
||||||
|
eventType);
|
||||||
|
if (tracePackets)
|
||||||
|
{
|
||||||
|
TraceAgc($"agc.dcb.event type=0x{eventType:X2} queues={triggered}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op == ItNop && register == RReleaseMem && length >= 7)
|
||||||
|
{
|
||||||
|
ApplySubmittedReleaseMem(ctx, currentAddress, tracePackets);
|
||||||
|
}
|
||||||
|
|
||||||
if (op == ItDrawIndexOffset2 &&
|
if (op == ItDrawIndexOffset2 &&
|
||||||
length >= 5 &&
|
length >= 5 &&
|
||||||
TryReadUInt32(ctx, currentAddress + 4, out var indexCount) &&
|
TryReadUInt32(ctx, currentAddress + 4, out var indexCount) &&
|
||||||
indexCount != 0)
|
indexCount != 0)
|
||||||
{
|
{
|
||||||
sawIndexedDraw = true;
|
sawIndexedDraw = true;
|
||||||
|
TryTranslateGuestDraw(ctx, state, indexCount, ref guestDrawKind);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op == ItNop &&
|
||||||
|
register == RDrawIndexAuto &&
|
||||||
|
length >= 2 &&
|
||||||
|
TryReadUInt32(ctx, currentAddress + 4, out var autoIndexCount) &&
|
||||||
|
autoIndexCount != 0)
|
||||||
|
{
|
||||||
|
sawIndexedDraw = true;
|
||||||
|
TryTranslateGuestDraw(ctx, state, autoIndexCount, ref guestDrawKind);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (op == ItNop && register == RFlip && length >= 6)
|
if (op == ItNop && register == RFlip && length >= 6)
|
||||||
@@ -654,6 +1092,18 @@ public static class AgcExports
|
|||||||
unchecked((int)videoOutHandle),
|
unchecked((int)videoOutHandle),
|
||||||
displayBufferIndex);
|
displayBufferIndex);
|
||||||
}
|
}
|
||||||
|
else if (sawIndexedDraw &&
|
||||||
|
guestDrawKind != GuestDrawKind.None &&
|
||||||
|
VideoOutExports.TryGetDisplayBufferInfo(
|
||||||
|
unchecked((int)videoOutHandle),
|
||||||
|
displayBufferIndex,
|
||||||
|
out var displayBuffer))
|
||||||
|
{
|
||||||
|
VulkanVideoPresenter.SubmitGuestDraw(
|
||||||
|
guestDrawKind,
|
||||||
|
displayBuffer.Width,
|
||||||
|
displayBuffer.Height);
|
||||||
|
}
|
||||||
|
|
||||||
_ = VideoOutExports.SubmitFlipFromAgc(ctx, unchecked((int)videoOutHandle), displayBufferIndex, unchecked((int)flipMode), flipArg);
|
_ = VideoOutExports.SubmitFlipFromAgc(ctx, unchecked((int)videoOutHandle), displayBufferIndex, unchecked((int)flipMode), flipArg);
|
||||||
}
|
}
|
||||||
@@ -662,6 +1112,152 @@ public static class AgcExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void ApplySubmittedReleaseMem(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong packetAddress,
|
||||||
|
bool tracePacket)
|
||||||
|
{
|
||||||
|
if (!TryReadUInt32(ctx, packetAddress + 8, out var control) ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + 12, out var destinationLo) ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + 16, out var destinationHi) ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + 20, out var dataLo) ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + 24, out var dataHi))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var dataSelection = (control >> 16) & 0xFFu;
|
||||||
|
var destinationAddress = ((ulong)destinationHi << 32) | destinationLo;
|
||||||
|
var data = ((ulong)dataHi << 32) | dataLo;
|
||||||
|
var wroteData = dataSelection switch
|
||||||
|
{
|
||||||
|
2 => TryWriteUInt32(ctx, destinationAddress, dataLo),
|
||||||
|
3 => ctx.TryWriteUInt64(destinationAddress, data),
|
||||||
|
_ => false,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (tracePacket)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.release_mem dst=0x{destinationAddress:X16} data_sel={dataSelection} " +
|
||||||
|
$"data=0x{data:X16} wrote={wroteData}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ApplySubmittedRegisters(
|
||||||
|
CpuContext ctx,
|
||||||
|
SubmittedDcbState state,
|
||||||
|
ulong packetAddress,
|
||||||
|
uint packetLength,
|
||||||
|
uint op,
|
||||||
|
uint register)
|
||||||
|
{
|
||||||
|
if (op == ItSetShReg)
|
||||||
|
{
|
||||||
|
if (packetLength < 3 ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + sizeof(uint), out var startRegister))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint index = 0; index < packetLength - 2; index++)
|
||||||
|
{
|
||||||
|
if (!TryReadUInt32(
|
||||||
|
ctx,
|
||||||
|
packetAddress + 8 + ((ulong)index * sizeof(uint)),
|
||||||
|
out var value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.ShRegisters[startRegister + index] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (op != ItNop ||
|
||||||
|
register is not (RCxRegsIndirect or RShRegsIndirect or RUcRegsIndirect) ||
|
||||||
|
packetLength < 4 ||
|
||||||
|
!TryReadUInt32(ctx, packetAddress + sizeof(uint), out var registerCount) ||
|
||||||
|
!TryReadUInt64(ctx, packetAddress + 8, out var registersAddress))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var destination = register switch
|
||||||
|
{
|
||||||
|
RCxRegsIndirect => state.CxRegisters,
|
||||||
|
RShRegsIndirect => state.ShRegisters,
|
||||||
|
_ => state.UcRegisters,
|
||||||
|
};
|
||||||
|
for (uint index = 0; index < registerCount; index++)
|
||||||
|
{
|
||||||
|
var entryAddress = registersAddress + ((ulong)index * 8);
|
||||||
|
if (!TryReadUInt32(ctx, entryAddress, out var registerOffset) ||
|
||||||
|
!TryReadUInt32(ctx, entryAddress + sizeof(uint), out var value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (registerOffset != 0)
|
||||||
|
{
|
||||||
|
destination[registerOffset] = value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryTranslateGuestDraw(
|
||||||
|
CpuContext ctx,
|
||||||
|
SubmittedDcbState state,
|
||||||
|
uint vertexCount,
|
||||||
|
ref GuestDrawKind drawKind)
|
||||||
|
{
|
||||||
|
if (drawKind != GuestDrawKind.None ||
|
||||||
|
vertexCount != 3 ||
|
||||||
|
!TryGetShaderAddress(state.ShRegisters, SpiShaderPgmLoEs, SpiShaderPgmHiEs, out var exportShaderAddress) ||
|
||||||
|
!TryGetShaderAddress(state.ShRegisters, SpiShaderPgmLoPs, SpiShaderPgmHiPs, out var pixelShaderAddress) ||
|
||||||
|
!state.CxRegisters.TryGetValue(SpiPsInputEna, out var psInputEna) ||
|
||||||
|
!state.CxRegisters.TryGetValue(SpiPsInputAddr, out var psInputAddr) ||
|
||||||
|
!Gen5ShaderTranslator.TryTranslate(
|
||||||
|
ctx,
|
||||||
|
exportShaderAddress,
|
||||||
|
pixelShaderAddress,
|
||||||
|
psInputEna,
|
||||||
|
psInputAddr,
|
||||||
|
out drawKind))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_submitTraceGate)
|
||||||
|
{
|
||||||
|
if (_tracedShaderTranslations.Add((exportShaderAddress, pixelShaderAddress, drawKind)))
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.shader_translated kind={drawKind} es=0x{exportShaderAddress:X16} " +
|
||||||
|
$"ps=0x{pixelShaderAddress:X16} vertices={vertexCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetShaderAddress(
|
||||||
|
IReadOnlyDictionary<uint, uint> registers,
|
||||||
|
uint loRegister,
|
||||||
|
uint hiRegister,
|
||||||
|
out ulong address)
|
||||||
|
{
|
||||||
|
address = 0;
|
||||||
|
if (!registers.TryGetValue(loRegister, out var lo) ||
|
||||||
|
!registers.TryGetValue(hiRegister, out var hi))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
address = ((ulong)hi << 40) | ((ulong)lo << 8);
|
||||||
|
return address != 0;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryReadTextureDescriptor(
|
private static bool TryReadTextureDescriptor(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong packetAddress,
|
ulong packetAddress,
|
||||||
@@ -728,7 +1324,11 @@ public static class AgcExports
|
|||||||
destination.Width > 8192 ||
|
destination.Width > 8192 ||
|
||||||
destination.Height > 8192 ||
|
destination.Height > 8192 ||
|
||||||
destination.TilingMode != 0 ||
|
destination.TilingMode != 0 ||
|
||||||
destination.PixelFormat is not (VideoOutPixelFormatA8R8G8B8Srgb or VideoOutPixelFormatA8B8G8R8Srgb))
|
destination.PixelFormat is not (
|
||||||
|
VideoOutPixelFormatA8R8G8B8Srgb or
|
||||||
|
VideoOutPixelFormatA8B8G8R8Srgb or
|
||||||
|
VideoOutPixelFormatB8G8R8A8Unorm or
|
||||||
|
VideoOutPixelFormatR8G8B8A8Unorm))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -765,7 +1365,9 @@ public static class AgcExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var destinationRow = new byte[checked((int)destinationPitch * 4)];
|
var destinationRow = new byte[checked((int)destinationPitch * 4)];
|
||||||
var rgbaDestination = destination.PixelFormat == VideoOutPixelFormatA8B8G8R8Srgb;
|
var rgbaDestination = destination.PixelFormat is
|
||||||
|
VideoOutPixelFormatA8B8G8R8Srgb or
|
||||||
|
VideoOutPixelFormatR8G8B8A8Unorm;
|
||||||
for (uint y = 0; y < destination.Height; y++)
|
for (uint y = 0; y < destination.Height; y++)
|
||||||
{
|
{
|
||||||
var sourceY = (uint)(((ulong)y * source.Height) / destination.Height);
|
var sourceY = (uint)(((ulong)y * source.Height) / destination.Height);
|
||||||
@@ -802,6 +1404,7 @@ public static class AgcExports
|
|||||||
_softwarePresenterFingerprints[fingerprintKey] = fingerprint;
|
_softwarePresenterFingerprints[fingerprintKey] = fingerprint;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
VideoOutExports.SubmitHostRgbaFrame(sourceBytes, source.Width, source.Height);
|
||||||
TraceAgc(
|
TraceAgc(
|
||||||
$"agc.software_presenter src=0x{source.Address:X16} {source.Width}x{source.Height} fmt={source.Format} " +
|
$"agc.software_presenter src=0x{source.Address:X16} {source.Width}x{source.Height} fmt={source.Format} " +
|
||||||
$"dst=0x{destination.Address:X16} {destination.Width}x{destination.Height} fingerprint=0x{fingerprint:X16}");
|
$"dst=0x{destination.Address:X16} {destination.Width}x{destination.Height} fingerprint=0x{fingerprint:X16}");
|
||||||
@@ -895,8 +1498,22 @@ public static class AgcExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var expectedLo = shaderType == 2 ? SpiShaderPgmLoEs : shaderType == 1 ? SpiShaderPgmLoPs : 0;
|
var expectedLo = shaderType switch
|
||||||
var expectedHi = shaderType == 2 ? SpiShaderPgmHiEs : shaderType == 1 ? SpiShaderPgmHiPs : 0;
|
{
|
||||||
|
0 => ComputePgmLo,
|
||||||
|
1 => SpiShaderPgmLoPs,
|
||||||
|
2 or 6 => SpiShaderPgmLoEs,
|
||||||
|
7 => SpiShaderPgmLoLs,
|
||||||
|
_ => 0u,
|
||||||
|
};
|
||||||
|
var expectedHi = shaderType switch
|
||||||
|
{
|
||||||
|
0 => ComputePgmHi,
|
||||||
|
1 => SpiShaderPgmHiPs,
|
||||||
|
2 or 6 => SpiShaderPgmHiEs,
|
||||||
|
7 => SpiShaderPgmHiLs,
|
||||||
|
_ => 0u,
|
||||||
|
};
|
||||||
if (expectedLo == 0 || loRegister != expectedLo || hiRegister != expectedHi)
|
if (expectedLo == 0 || loRegister != expectedLo || hiRegister != expectedHi)
|
||||||
{
|
{
|
||||||
TraceCreateShader(0, headerAddress, codeAddress, $"unexpected-registers type={shaderType} lo=0x{loRegister:X8} hi=0x{hiRegister:X8}");
|
TraceCreateShader(0, headerAddress, codeAddress, $"unexpected-registers type={shaderType} lo=0x{loRegister:X8} hi=0x{hiRegister:X8}");
|
||||||
@@ -909,6 +1526,9 @@ public static class AgcExports
|
|||||||
TryWriteUInt32(ctx, shRegistersAddress + 8 + sizeof(uint), hiValue);
|
TryWriteUInt32(ctx, shRegistersAddress + 8 + sizeof(uint), hiValue);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool IsEsGeometryShaderType(byte shaderType) =>
|
||||||
|
shaderType is 2 or 6;
|
||||||
|
|
||||||
private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace)
|
private static int SetIndirectPatchAddress(CpuContext ctx, string registerSpace)
|
||||||
{
|
{
|
||||||
var commandAddress = ctx[CpuRegister.Rdi];
|
var commandAddress = ctx[CpuRegister.Rdi];
|
||||||
@@ -1031,6 +1651,153 @@ public static class AgcExports
|
|||||||
return ctx.TryWriteUInt64(fieldAddress, fieldAddress + relativeAddress);
|
return ctx.TryWriteUInt64(fieldAddress, fieldAddress + relativeAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int ReturnRegisterDefaults(CpuContext ctx, bool internalDefaults)
|
||||||
|
{
|
||||||
|
var version = (uint)ctx[CpuRegister.Rdi];
|
||||||
|
if (!IsSupportedRegisterDefaultsVersion(version))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetRegisterDefaultsAllocation(ctx, out var allocation))
|
||||||
|
{
|
||||||
|
return ReturnPointer(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = internalDefaults ? allocation.Internal : allocation.Primary;
|
||||||
|
TraceAgc($"agc.get_register_defaults internal={internalDefaults} version={version} address=0x{address:X16}");
|
||||||
|
return ReturnPointer(ctx, address);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSupportedRegisterDefaultsVersion(uint version)
|
||||||
|
{
|
||||||
|
return version is
|
||||||
|
RegisterDefaultsVersion7 or
|
||||||
|
RegisterDefaultsVersion8 or
|
||||||
|
RegisterDefaultsVersion10;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetRegisterDefaultsAllocation(
|
||||||
|
CpuContext ctx,
|
||||||
|
out RegisterDefaultsAllocation allocation)
|
||||||
|
{
|
||||||
|
lock (_registerDefaultsGate)
|
||||||
|
{
|
||||||
|
if (_registerDefaultsAllocations.TryGetValue(ctx.Memory, out allocation!))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryBuildRegisterDefaults(
|
||||||
|
ctx,
|
||||||
|
PrimaryRegisterDefaults,
|
||||||
|
cxTableLength: 78,
|
||||||
|
shTableLength: 29,
|
||||||
|
ucTableLength: 20,
|
||||||
|
out var primaryAddress) ||
|
||||||
|
!TryBuildRegisterDefaults(
|
||||||
|
ctx,
|
||||||
|
InternalRegisterDefaults,
|
||||||
|
cxTableLength: 4,
|
||||||
|
shTableLength: 15,
|
||||||
|
ucTableLength: 3,
|
||||||
|
out var internalAddress))
|
||||||
|
{
|
||||||
|
allocation = null!;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
allocation = new RegisterDefaultsAllocation(primaryAddress, internalAddress);
|
||||||
|
_registerDefaultsAllocations.Add(ctx.Memory, allocation);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryBuildRegisterDefaults(
|
||||||
|
CpuContext ctx,
|
||||||
|
RegisterDefaultGroup[] groups,
|
||||||
|
int cxTableLength,
|
||||||
|
int shTableLength,
|
||||||
|
int ucTableLength,
|
||||||
|
out ulong address)
|
||||||
|
{
|
||||||
|
var cxTableOffset = AlignUp(RegisterDefaultsSize, sizeof(ulong));
|
||||||
|
var shTableOffset = cxTableOffset + (cxTableLength * sizeof(ulong));
|
||||||
|
var ucTableOffset = shTableOffset + (shTableLength * sizeof(ulong));
|
||||||
|
var typesOffset = AlignUp(ucTableOffset + (ucTableLength * sizeof(ulong)), sizeof(uint));
|
||||||
|
var registerBlocksOffset = AlignUp(typesOffset + (groups.Length * 3 * sizeof(uint)), sizeof(ulong));
|
||||||
|
var blobLength = registerBlocksOffset + (groups.Length * RegisterDefaultBlockSize);
|
||||||
|
|
||||||
|
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, (ulong)blobLength, 0x1000, out address))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var blob = new byte[blobLength];
|
||||||
|
WriteBlobUInt64(blob, 0x00, address + (ulong)cxTableOffset);
|
||||||
|
WriteBlobUInt64(blob, 0x08, address + (ulong)shTableOffset);
|
||||||
|
WriteBlobUInt64(blob, 0x10, address + (ulong)ucTableOffset);
|
||||||
|
WriteBlobUInt64(blob, 0x30, address + (ulong)typesOffset);
|
||||||
|
WriteBlobUInt32(blob, 0x38, (uint)groups.Length);
|
||||||
|
|
||||||
|
for (var groupIndex = 0; groupIndex < groups.Length; groupIndex++)
|
||||||
|
{
|
||||||
|
var group = groups[groupIndex];
|
||||||
|
if (group.Registers.Length > 16)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tableOffset = group.Space switch
|
||||||
|
{
|
||||||
|
0 => cxTableOffset,
|
||||||
|
1 => shTableOffset,
|
||||||
|
2 => ucTableOffset,
|
||||||
|
_ => -1,
|
||||||
|
};
|
||||||
|
var tableLength = group.Space switch
|
||||||
|
{
|
||||||
|
0 => cxTableLength,
|
||||||
|
1 => shTableLength,
|
||||||
|
2 => ucTableLength,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
if (tableOffset < 0 || group.Index >= tableLength)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var registerBlockOffset = registerBlocksOffset + (groupIndex * RegisterDefaultBlockSize);
|
||||||
|
WriteBlobUInt64(
|
||||||
|
blob,
|
||||||
|
tableOffset + ((int)group.Index * sizeof(ulong)),
|
||||||
|
address + (ulong)registerBlockOffset);
|
||||||
|
|
||||||
|
var typeEntryOffset = typesOffset + (groupIndex * 3 * sizeof(uint));
|
||||||
|
WriteBlobUInt32(blob, typeEntryOffset, group.Type);
|
||||||
|
WriteBlobUInt32(blob, typeEntryOffset + sizeof(uint), (group.Index * 4) + group.Space);
|
||||||
|
|
||||||
|
for (var registerIndex = 0; registerIndex < group.Registers.Length; registerIndex++)
|
||||||
|
{
|
||||||
|
var register = group.Registers[registerIndex];
|
||||||
|
var registerOffset = registerBlockOffset + (registerIndex * 2 * sizeof(uint));
|
||||||
|
WriteBlobUInt32(blob, registerOffset, register.Offset);
|
||||||
|
WriteBlobUInt32(blob, registerOffset + sizeof(uint), register.Value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(address, blob);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int AlignUp(int value, int alignment) =>
|
||||||
|
(value + alignment - 1) & -alignment;
|
||||||
|
|
||||||
|
private static void WriteBlobUInt32(Span<byte> blob, int offset, uint value) =>
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(blob[offset..], value);
|
||||||
|
|
||||||
|
private static void WriteBlobUInt64(Span<byte> blob, int offset, ulong value) =>
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(blob[offset..], value);
|
||||||
|
|
||||||
private static int ReturnPointer(CpuContext ctx, ulong pointer)
|
private static int ReturnPointer(CpuContext ctx, ulong pointer)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = pointer;
|
ctx[CpuRegister.Rax] = pointer;
|
||||||
@@ -1110,7 +1877,9 @@ public static class AgcExports
|
|||||||
|
|
||||||
private static void TraceCreateShader(ulong destinationAddress, ulong headerAddress, ulong codeAddress, string detail)
|
private static void TraceCreateShader(ulong destinationAddress, ulong headerAddress, ulong codeAddress, string detail)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal))
|
var isOk = string.Equals(detail, "ok", StringComparison.Ordinal);
|
||||||
|
if (isOk &&
|
||||||
|
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,73 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.VideoOut;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
|
internal static class Gen5ShaderTranslator
|
||||||
|
{
|
||||||
|
private static readonly uint[] FullscreenBarycentricEs =
|
||||||
|
[
|
||||||
|
0xBFA00001, 0x7E000000, 0x7E000000, 0x7E000000,
|
||||||
|
0x93EBFF03, 0x00080008, 0x8F6A8C6B, 0x8700FF03,
|
||||||
|
0x000000FF, 0x887C6A00, 0xBF900009, 0x81EA6BC0,
|
||||||
|
0x90FE6AC1, 0xF8000941, 0x00000000, 0x81EA00C0,
|
||||||
|
0xBF8CFF0F, 0x90FE6AC1, 0x36040A81, 0x2C060A81,
|
||||||
|
0x7E000280, 0x7E0202F2, 0xD7460002, 0x03050302,
|
||||||
|
0xD7460003, 0x03050303, 0x7E040B02, 0x7E060B03,
|
||||||
|
0xF80008CF, 0x01000302, 0xBF810000,
|
||||||
|
];
|
||||||
|
|
||||||
|
private static readonly uint[] FullscreenBarycentricPs =
|
||||||
|
[
|
||||||
|
0xD52F0000, 0x00000200,
|
||||||
|
0xD52F0001, 0x00000602,
|
||||||
|
0xF8001C0F, 0x00000100,
|
||||||
|
0xBF810000,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static bool TryTranslate(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong exportShaderAddress,
|
||||||
|
ulong pixelShaderAddress,
|
||||||
|
uint psInputEna,
|
||||||
|
uint psInputAddr,
|
||||||
|
out GuestDrawKind drawKind)
|
||||||
|
{
|
||||||
|
drawKind = GuestDrawKind.None;
|
||||||
|
if (exportShaderAddress == 0 ||
|
||||||
|
pixelShaderAddress == 0 ||
|
||||||
|
psInputEna != 0x00000002 ||
|
||||||
|
psInputAddr != 0x00000002 ||
|
||||||
|
!MatchesProgram(ctx, exportShaderAddress, FullscreenBarycentricEs) ||
|
||||||
|
!MatchesProgram(ctx, pixelShaderAddress, FullscreenBarycentricPs))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
drawKind = GuestDrawKind.FullscreenBarycentric;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchesProgram(CpuContext ctx, ulong address, ReadOnlySpan<uint> expected)
|
||||||
|
{
|
||||||
|
var bytes = new byte[expected.Length * sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < expected.Length; index++)
|
||||||
|
{
|
||||||
|
if (BinaryPrimitives.ReadUInt32LittleEndian(bytes.AsSpan(index * sizeof(uint))) != expected[index])
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Kernel;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
@@ -17,9 +18,13 @@ public static class AmprExports
|
|||||||
private const ulong CommandBufferAux1Offset = 0x20;
|
private const ulong CommandBufferAux1Offset = 0x20;
|
||||||
private const ulong ReadFileRecordSize = 0x30;
|
private const ulong ReadFileRecordSize = 0x30;
|
||||||
private const ulong KernelEventQueueRecordSize = 0x30;
|
private const ulong KernelEventQueueRecordSize = 0x30;
|
||||||
|
private const ulong WriteAddressRecordSize = 0x20;
|
||||||
private const uint ReadFileRecordType = 1;
|
private const uint ReadFileRecordType = 1;
|
||||||
private const uint KernelEventQueueRecordType = 2;
|
private const uint KernelEventQueueRecordType = 2;
|
||||||
|
private const uint WriteAddressRecordType = 3;
|
||||||
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
||||||
|
private static readonly bool _traceAmpr =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
private sealed class CommandBufferState
|
private sealed class CommandBufferState
|
||||||
{
|
{
|
||||||
@@ -72,12 +77,7 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
var buffer = 0UL;
|
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer: 0, size: 0, aux0, aux1, clear: false))
|
||||||
var size = 0UL;
|
|
||||||
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer);
|
|
||||||
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size);
|
|
||||||
|
|
||||||
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0, aux1, clear: false))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -101,8 +101,9 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, 0) ||
|
Span<byte> auxiliaryPointers = stackalloc byte[sizeof(ulong) * 2];
|
||||||
!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, 0))
|
auxiliaryPointers.Clear();
|
||||||
|
if (!ctx.Memory.TryWrite(commandBuffer + CommandBufferAux0Offset, auxiliaryPointers))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -176,8 +177,15 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out var buffer) ||
|
Span<byte> bufferPointers = stackalloc byte[sizeof(ulong) * 2];
|
||||||
!ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out var size) ||
|
if (!ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, bufferPointers))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers);
|
||||||
|
var size = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers[sizeof(ulong)..]);
|
||||||
|
if (
|
||||||
!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0))
|
!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
@@ -284,6 +292,18 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "C+IEj+BsAFM",
|
||||||
|
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAmpr")]
|
||||||
|
public static int MeasureCommandSizeWriteAddressOnCompletion(CpuContext ctx)
|
||||||
|
{
|
||||||
|
TraceAmpr(ctx, "measure_write_address_complete", 0, WriteAddressRecordSize, 0);
|
||||||
|
ctx[CpuRegister.Rax] = WriteAddressRecordSize;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "tZDDEo2tE5k",
|
Nid = "tZDDEo2tE5k",
|
||||||
ExportName = "sceAmprCommandBufferGetSize",
|
ExportName = "sceAmprCommandBufferGetSize",
|
||||||
@@ -340,28 +360,116 @@ public static class AmprExports
|
|||||||
var commandBuffer = ctx[CpuRegister.Rdi];
|
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||||
var equeue = ctx[CpuRegister.Rsi];
|
var equeue = ctx[CpuRegister.Rsi];
|
||||||
var ident = ctx[CpuRegister.Rdx];
|
var ident = ctx[CpuRegister.Rdx];
|
||||||
var filter = ctx[CpuRegister.Rcx];
|
var completionToken = ctx[CpuRegister.Rcx];
|
||||||
var userData = ctx[CpuRegister.R8];
|
var userData = ctx[CpuRegister.R8];
|
||||||
var data = ctx[CpuRegister.R9];
|
|
||||||
|
|
||||||
if (commandBuffer == 0)
|
if (commandBuffer == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var extra = 0UL;
|
if (!AppendKernelEventQueueRecord(
|
||||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out extra);
|
ctx,
|
||||||
|
commandBuffer,
|
||||||
if (!AppendKernelEventQueueRecord(ctx, commandBuffer, equeue, ident, filter, userData, data, extra))
|
equeue,
|
||||||
|
ident,
|
||||||
|
completionToken,
|
||||||
|
userData))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAmpr(ctx, "write_equeue", commandBuffer, equeue, ident);
|
TraceAmpr(ctx, "write_equeue", commandBuffer, ident, completionToken);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sJXyWHjP-F8",
|
||||||
|
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAmpr")]
|
||||||
|
public static int CommandBufferWriteAddressOnCompletion(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||||
|
var address = ctx[CpuRegister.Rsi];
|
||||||
|
var value = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
if (commandBuffer == 0 || address == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!AppendWriteAddressRecord(ctx, commandBuffer, address, value))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAmpr(ctx, "write_address_complete", commandBuffer, address, value);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int CompleteCommandBuffer(CpuContext ctx, ulong commandBuffer)
|
||||||
|
{
|
||||||
|
if (commandBuffer == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetCommandBufferState(ctx, commandBuffer, out var buffer, out _, out var state) || state is null)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong writeOffset;
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
writeOffset = state.WriteOffset;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = 0UL;
|
||||||
|
while (offset < writeOffset)
|
||||||
|
{
|
||||||
|
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
switch (recordType)
|
||||||
|
{
|
||||||
|
case ReadFileRecordType:
|
||||||
|
offset += ReadFileRecordSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case KernelEventQueueRecordType:
|
||||||
|
if (!CompleteKernelEventQueueRecord(ctx, buffer + offset))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += KernelEventQueueRecordSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
case WriteAddressRecordType:
|
||||||
|
if (!CompleteWriteAddressRecord(ctx, buffer + offset))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += WriteAddressRecordSize;
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
TraceAmpr(ctx, "complete_unknown", commandBuffer, recordType, offset);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAmpr(ctx, "complete", commandBuffer, buffer, writeOffset);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool InitializeCommandBuffer(
|
private static bool InitializeCommandBuffer(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong commandBuffer,
|
ulong commandBuffer,
|
||||||
@@ -371,20 +479,34 @@ public static class AmprExports
|
|||||||
ulong aux1,
|
ulong aux1,
|
||||||
bool clear)
|
bool clear)
|
||||||
{
|
{
|
||||||
|
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
|
||||||
if (clear)
|
if (clear)
|
||||||
{
|
{
|
||||||
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
|
|
||||||
header.Clear();
|
header.Clear();
|
||||||
if (!ctx.Memory.TryWrite(commandBuffer, header))
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(commandBuffer, header))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buffer = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferDataOffset..]);
|
||||||
|
size = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferSizeOffset..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
|
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSelfOffset..], commandBuffer);
|
||||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, aux0) &&
|
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferDataOffset..], buffer);
|
||||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, aux1) &&
|
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSizeOffset..], size);
|
||||||
WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0);
|
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux0Offset..], aux0);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux1Offset..], aux1);
|
||||||
|
if (!ctx.Memory.TryWrite(commandBuffer, header))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset: 0);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
||||||
@@ -399,6 +521,26 @@ public static class AmprExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
||||||
|
{
|
||||||
|
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 3];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(pointers, commandBuffer);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(pointers[sizeof(ulong)..], buffer);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(pointers[(sizeof(ulong) * 2)..], size);
|
||||||
|
return ctx.Memory.TryWrite(commandBuffer + CommandBufferSelfOffset, pointers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateCommandBufferState(
|
||||||
|
ulong commandBuffer,
|
||||||
|
ulong buffer,
|
||||||
|
ulong size,
|
||||||
|
ulong writeOffset)
|
||||||
|
{
|
||||||
var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
@@ -406,15 +548,6 @@ public static class AmprExports
|
|||||||
state.Size = size;
|
state.Size = size;
|
||||||
state.WriteOffset = writeOffset;
|
state.WriteOffset = writeOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
|
||||||
{
|
|
||||||
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
|
|
||||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferDataOffset, buffer) &&
|
|
||||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferSizeOffset, size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGetCommandBufferState(
|
private static bool TryGetCommandBufferState(
|
||||||
@@ -435,9 +568,11 @@ public static class AmprExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer) &&
|
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 2];
|
||||||
ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size))
|
if (ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, pointers))
|
||||||
{
|
{
|
||||||
|
buffer = BinaryPrimitives.ReadUInt64LittleEndian(pointers);
|
||||||
|
size = BinaryPrimitives.ReadUInt64LittleEndian(pointers[sizeof(ulong)..]);
|
||||||
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
@@ -558,20 +693,28 @@ public static class AmprExports
|
|||||||
ulong commandBuffer,
|
ulong commandBuffer,
|
||||||
ulong equeue,
|
ulong equeue,
|
||||||
ulong ident,
|
ulong ident,
|
||||||
ulong filter,
|
ulong completionToken,
|
||||||
ulong userData,
|
ulong userData)
|
||||||
ulong data,
|
|
||||||
ulong extra)
|
|
||||||
{
|
{
|
||||||
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
|
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
|
||||||
record.Clear();
|
record.Clear();
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType);
|
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], unchecked((uint)filter));
|
BinaryPrimitives.WriteInt16LittleEndian(record[0x04..], KernelEventQueueCompatExports.KernelEventFilterAmpr);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], equeue);
|
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], equeue);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident);
|
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData);
|
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], data);
|
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], completionToken);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x28..], extra);
|
|
||||||
|
return AppendCommandBufferRecord(ctx, commandBuffer, record);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool AppendWriteAddressRecord(CpuContext ctx, ulong commandBuffer, ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> record = stackalloc byte[(int)WriteAddressRecordSize];
|
||||||
|
record.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], WriteAddressRecordType);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], address);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], value);
|
||||||
|
|
||||||
return AppendCommandBufferRecord(ctx, commandBuffer, record);
|
return AppendCommandBufferRecord(ctx, commandBuffer, record);
|
||||||
}
|
}
|
||||||
@@ -604,9 +747,69 @@ public static class AmprExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool CompleteKernelEventQueueRecord(CpuContext ctx, ulong recordAddress)
|
||||||
|
{
|
||||||
|
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
|
||||||
|
if (!ctx.Memory.TryRead(recordAddress, record))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var filter = unchecked((short)BinaryPrimitives.ReadUInt32LittleEndian(record[0x04..]));
|
||||||
|
var equeue = BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]);
|
||||||
|
var ident = BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]);
|
||||||
|
var userData = BinaryPrimitives.ReadUInt64LittleEndian(record[0x18..]);
|
||||||
|
var data = BinaryPrimitives.ReadUInt64LittleEndian(record[0x20..]);
|
||||||
|
var extra = BinaryPrimitives.ReadUInt64LittleEndian(record[0x28..]);
|
||||||
|
|
||||||
|
var queuedEvent = new KernelEventQueueCompatExports.KernelQueuedEvent(
|
||||||
|
ident,
|
||||||
|
filter,
|
||||||
|
0x20,
|
||||||
|
unchecked((uint)extra),
|
||||||
|
data,
|
||||||
|
userData);
|
||||||
|
|
||||||
|
_ = KernelEventQueueCompatExports.EnqueueEvent(equeue, queuedEvent);
|
||||||
|
TraceAmpr(ctx, "complete_equeue", equeue, ident, data);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool CompleteWriteAddressRecord(CpuContext ctx, ulong recordAddress)
|
||||||
|
{
|
||||||
|
Span<byte> record = stackalloc byte[(int)WriteAddressRecordSize];
|
||||||
|
if (!ctx.Memory.TryRead(recordAddress, record))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var address = BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]);
|
||||||
|
var value = BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]);
|
||||||
|
if (!ctx.TryWriteUInt64(address, value))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAmpr(ctx, "complete_write_address", address, value, 0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
|
if (!_traceAmpr)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -628,7 +831,7 @@ public static class AmprExports
|
|||||||
string? hostPath,
|
string? hostPath,
|
||||||
int result)
|
int result)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
|
if (!_traceAmpr)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,6 +4,7 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.AppContent;
|
namespace SharpEmu.Libs.AppContent;
|
||||||
|
|
||||||
@@ -11,6 +12,8 @@ public static class AppContentExports
|
|||||||
{
|
{
|
||||||
private const ulong BootParamAttrOffset = 4;
|
private const ulong BootParamAttrOffset = 4;
|
||||||
private const string Temp0MountPoint = "/temp0";
|
private const string Temp0MountPoint = "/temp0";
|
||||||
|
private const uint AppParamSkuFlag = 0;
|
||||||
|
private const int AppParamSkuFlagFull = 3;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "R9lA82OraNs",
|
Nid = "R9lA82OraNs",
|
||||||
@@ -59,6 +62,41 @@ public static class AppContentExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "99b82IKXpH4",
|
||||||
|
ExportName = "sceAppContentAppParamGetInt",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceAppContent")]
|
||||||
|
public static int AppContentAppParamGetInt(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var paramId = (uint)ctx[CpuRegister.Rdi];
|
||||||
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (valueAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
int value;
|
||||||
|
if (paramId == AppParamSkuFlag)
|
||||||
|
{
|
||||||
|
value = AppParamSkuFlagFull;
|
||||||
|
}
|
||||||
|
else if (!TryReadUserDefinedParam(paramId, out value))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||||
|
if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAppContent($"app_param_get_int id={paramId} value={value}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "buYbeLOGWmA",
|
Nid = "buYbeLOGWmA",
|
||||||
ExportName = "sceAppContentTemporaryDataMount2",
|
ExportName = "sceAppContentTemporaryDataMount2",
|
||||||
@@ -83,6 +121,69 @@ public static class AppContentExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUserDefinedParam(uint paramId, out int value)
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
if (paramId is < 1 or > 4)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||||
|
if (string.IsNullOrWhiteSpace(app0Root))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var paramJsonPath = Path.Combine(app0Root, "sce_sys", "param.json");
|
||||||
|
if (!File.Exists(paramJsonPath))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var stream = File.OpenRead(paramJsonPath);
|
||||||
|
using var document = JsonDocument.Parse(stream);
|
||||||
|
var propertyName = $"userDefinedParam{paramId}";
|
||||||
|
if (document.RootElement.TryGetProperty(propertyName, out var element) &&
|
||||||
|
element.TryGetInt32(out var parsedValue))
|
||||||
|
{
|
||||||
|
value = parsedValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (JsonException)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceAppContent(string message)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] app_content.{message}");
|
||||||
|
}
|
||||||
|
|
||||||
private static string ResolveTemp0Root()
|
private static string ResolveTemp0Root()
|
||||||
{
|
{
|
||||||
const string temp0VariableName = "SHARPEMU_TEMP0_DIR";
|
const string temp0VariableName = "SHARPEMU_TEMP0_DIR";
|
||||||
|
|||||||
@@ -0,0 +1,231 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
|
public static class AudioOut2Exports
|
||||||
|
{
|
||||||
|
private const int AudioOut2ContextParamSize = 0x80;
|
||||||
|
private const int AudioOut2ContextMemorySize = 0x10000;
|
||||||
|
private const int AudioOut2ContextMemoryAlignment = 0x10000;
|
||||||
|
private static long _nextContextHandle = 1;
|
||||||
|
private static long _nextUserHandle = 1;
|
||||||
|
private static int _nextPortId;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "g2tViFIohHE",
|
||||||
|
ExportName = "sceAudioOut2Initialize",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2Initialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "t5YrizufpQc",
|
||||||
|
ExportName = "sceAudioOut2ContextResetParam",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextResetParam(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (paramAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||||
|
param.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x00..], AudioOut2ContextParamSize);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x04..], 2);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x08..], 48000);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(paramAddress, param)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "pDmme7Bgm6E",
|
||||||
|
ExportName = "sceAudioOut2ContextQueryMemory",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> memoryInfo = stackalloc byte[0x20];
|
||||||
|
memoryInfo.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0x6o1VVAYSY",
|
||||||
|
ExportName = "sceAudioOut2ContextCreate",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var memoryAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var memorySize = ctx[CpuRegister.Rdx];
|
||||||
|
var outContextAddress = ctx[CpuRegister.Rcx];
|
||||||
|
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||||
|
return TryWriteUInt64(ctx, outContextAddress, handle)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "on6ZH7Abo10",
|
||||||
|
ExportName = "sceAudioOut2ContextDestroy",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JK2wamZPzwM",
|
||||||
|
ExportName = "sceAudioOut2PortCreate",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2PortCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var type = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var paramAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var outPortAddress = ctx[CpuRegister.Rdx];
|
||||||
|
var contextAddress = ctx[CpuRegister.Rcx];
|
||||||
|
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
||||||
|
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
||||||
|
return TryWriteUInt64(ctx, outPortAddress, handle)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gatEUKG+Ea4",
|
||||||
|
ExportName = "sceAudioOut2PortGetState",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2PortGetState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle == 0 || stateAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = (int)((handle >> 16) & 0xFF);
|
||||||
|
Span<byte> state = stackalloc byte[0x20];
|
||||||
|
state.Clear();
|
||||||
|
var output = type == 2 ? 0x40 : 0x01;
|
||||||
|
var channels = type == 2 ? 1 : 2;
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], unchecked((ushort)output));
|
||||||
|
state[0x02] = unchecked((byte)channels);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(stateAddress, state)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DImz2Ft9E2g",
|
||||||
|
ExportName = "sceAudioOut2GetSpeakerInfo",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2GetSpeakerInfo(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var infoAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (infoAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> info = stackalloc byte[0x40];
|
||||||
|
info.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 1);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 2);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(infoAddress, info)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cd+Rtw+D1x8",
|
||||||
|
ExportName = "sceAudioOut2PortDestroy",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "IaZXJ9M79uo",
|
||||||
|
ExportName = "sceAudioOut2UserDestroy",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "xywYcRB7nbQ",
|
||||||
|
ExportName = "sceAudioOut2UserCreate",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2UserCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||||
|
return TryWriteUInt64(ctx, outUserAddress, handle)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.CommonDialog;
|
||||||
|
|
||||||
|
public static class CommonDialogExports
|
||||||
|
{
|
||||||
|
private const int AlreadySystemInitialized = unchecked((int)0x80B80002);
|
||||||
|
private static int _initialized;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uoUpLGNkygk",
|
||||||
|
ExportName = "sceCommonDialogInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceCommonDialog")]
|
||||||
|
public static int CommonDialogInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
||||||
|
? 0
|
||||||
|
: AlreadySystemInitialized;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "BQ3tey0JmQM",
|
||||||
|
ExportName = "sceCommonDialogIsUsed",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceCommonDialog")]
|
||||||
|
public static int CommonDialogIsUsed(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.GameUpdate;
|
||||||
|
|
||||||
|
public static class GameUpdateExports
|
||||||
|
{
|
||||||
|
private static int _initialized;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "YJtKLttI9fM",
|
||||||
|
ExportName = "sceGameUpdateInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceGameUpdate")]
|
||||||
|
public static int GameUpdateInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _initialized, 1);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Json;
|
||||||
|
|
||||||
|
public static class JsonExports
|
||||||
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-hJRce8wn1U",
|
||||||
|
ExportName = "_ZN3sce4Json12MemAllocatorC2Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int MemAllocatorConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
TraceJson("MemAllocator.ctor", thisAddress, 0);
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "OcAgPxcq5Vk",
|
||||||
|
ExportName = "_ZN3sce4Json12MemAllocatorD2Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int MemAllocatorDestructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
TraceJson("MemAllocator.dtor", ctx[CpuRegister.Rdi], 0);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cK6bYHf-Q5E",
|
||||||
|
ExportName = "_ZN3sce4Json11InitializerC1Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int InitializerConstructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
TraceJson("Initializer.ctor", thisAddress, 0);
|
||||||
|
ctx[CpuRegister.Rax] = thisAddress;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "RujUxbr3haM",
|
||||||
|
ExportName = "_ZN3sce4Json11InitializerD1Ev",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int InitializerDestructor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
TraceJson("Initializer.dtor", ctx[CpuRegister.Rdi], 0);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Cxwy7wHq4J0",
|
||||||
|
ExportName = "_ZN3sce4Json11Initializer10initializeEPKNS0_13InitParameterE",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceJson")]
|
||||||
|
public static int InitializerInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thisAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var initParameterAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (thisAddress == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceJson("Initializer.initialize", thisAddress, initParameterAddress);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceJson(string operation, ulong thisAddress, ulong argument)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_JSON"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] json.{operation} this=0x{thisAddress:X16} arg=0x{argument:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Ampr;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
@@ -10,8 +11,13 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
|
|
||||||
public static class KernelAprCompatExports
|
public static class KernelAprCompatExports
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<uint, ulong> _submittedCommandBuffers = new();
|
private static readonly ConcurrentDictionary<uint, AprSubmission> _submittedCommandBuffers = new();
|
||||||
private static int _nextSubmissionId;
|
private static int _nextSubmissionId;
|
||||||
|
private static int _aprWaitTraceCount;
|
||||||
|
private static readonly bool _traceApr =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private readonly record struct AprSubmission(ulong CommandBuffer, ulong Priority, ulong ResultAddress);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ASoW5WE-UPo",
|
Nid = "ASoW5WE-UPo",
|
||||||
@@ -36,7 +42,13 @@ public static class KernelAprCompatExports
|
|||||||
submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
_submittedCommandBuffers[submissionId] = commandBuffer;
|
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, priority, resultAddress);
|
||||||
|
|
||||||
|
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
||||||
|
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return completionResult;
|
||||||
|
}
|
||||||
|
|
||||||
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||||
{
|
{
|
||||||
@@ -60,20 +72,23 @@ public static class KernelAprCompatExports
|
|||||||
public static int KernelAprWaitCommandBuffer(CpuContext ctx)
|
public static int KernelAprWaitCommandBuffer(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
var priority = ctx[CpuRegister.Rsi];
|
var waitArg1 = ctx[CpuRegister.Rsi];
|
||||||
var resultAddress = ctx[CpuRegister.Rdx];
|
var waitArg2 = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
if (!_submittedCommandBuffers.TryRemove(submissionId, out var commandBuffer))
|
if (!_submittedCommandBuffers.TryRemove(submissionId, out var submission))
|
||||||
{
|
{
|
||||||
|
TraceAprWaitFailure(ctx, "wait_missing", submissionId, commandBuffer: 0, waitArg1, waitArg2);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress);
|
||||||
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
|
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
|
||||||
{
|
{
|
||||||
|
TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceApr(ctx, "wait", submissionId, commandBuffer, priority, resultAddress);
|
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,7 +106,13 @@ public static class KernelAprCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
||||||
_submittedCommandBuffers[submissionId] = commandBuffer;
|
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
|
||||||
|
|
||||||
|
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
||||||
|
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return completionResult;
|
||||||
|
}
|
||||||
|
|
||||||
TraceApr(ctx, "submit", submissionId, commandBuffer, ctx[CpuRegister.Rsi], 0);
|
TraceApr(ctx, "submit", submissionId, commandBuffer, ctx[CpuRegister.Rsi], 0);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -112,7 +133,13 @@ public static class KernelAprCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
||||||
_submittedCommandBuffers[submissionId] = commandBuffer;
|
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
|
||||||
|
|
||||||
|
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
||||||
|
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return completionResult;
|
||||||
|
}
|
||||||
|
|
||||||
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||||
{
|
{
|
||||||
@@ -130,6 +157,27 @@ public static class KernelAprCompatExports
|
|||||||
return ctx.Memory.TryWrite(resultAddress, result);
|
return ctx.Memory.TryWrite(resultAddress, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress)
|
||||||
|
{
|
||||||
|
if (waitArg2 == 0)
|
||||||
|
{
|
||||||
|
return submittedResultAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF)
|
||||||
|
{
|
||||||
|
return submittedResultAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
return waitArg2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAmprCompletionToken(ulong value)
|
||||||
|
{
|
||||||
|
var tag = value >> 56;
|
||||||
|
return tag is 0x0C or 0x10;
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
@@ -145,7 +193,7 @@ public static class KernelAprCompatExports
|
|||||||
ulong priority,
|
ulong priority,
|
||||||
ulong aux)
|
ulong aux)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
|
if (!_traceApr)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -154,5 +202,54 @@ public static class KernelAprCompatExports
|
|||||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||||
Console.Error.WriteLine(
|
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}");
|
$"[LOADER][TRACE] apr.{operation}: id=0x{submissionId:X8} cmd=0x{commandBuffer:X16} priority=0x{priority:X16} aux=0x{aux:X16} ret=0x{returnRip:X16}");
|
||||||
|
if (aux != 0 &&
|
||||||
|
ctx.TryReadUInt64(aux, out var result0) &&
|
||||||
|
ctx.TryReadUInt64(aux + sizeof(ulong), out var result1))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] apr.{operation}.result: addr=0x{aux:X16} q0=0x{result0:X16} q1=0x{result1:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceAprWaitFailure(
|
||||||
|
CpuContext ctx,
|
||||||
|
string operation,
|
||||||
|
uint submissionId,
|
||||||
|
ulong commandBuffer,
|
||||||
|
ulong priority,
|
||||||
|
ulong resultAddress)
|
||||||
|
{
|
||||||
|
if (!_traceApr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var traceCount = Interlocked.Increment(ref _aprWaitTraceCount);
|
||||||
|
if (traceCount > 32 && (traceCount & 0x3FF) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var returnRip = 0UL;
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] apr.{operation}: id=0x{submissionId:X8} cmd=0x{commandBuffer:X16} " +
|
||||||
|
$"rsi=0x{priority:X16} rdx=0x{resultAddress:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16} " +
|
||||||
|
$"r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16} ret=0x{returnRip:X16}");
|
||||||
|
TraceReadableQword(ctx, operation, "rsi", priority);
|
||||||
|
TraceReadableQword(ctx, operation, "rdx", resultAddress);
|
||||||
|
TraceReadableQword(ctx, operation, "rcx", ctx[CpuRegister.Rcx]);
|
||||||
|
TraceReadableQword(ctx, operation, "r8", ctx[CpuRegister.R8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceReadableQword(CpuContext ctx, string operation, string name, ulong address)
|
||||||
|
{
|
||||||
|
if (address == 0 || !ctx.TryReadUInt64(address, out var value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] apr.{operation}.{name}: addr=0x{address:X16} q0=0x{value:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,638 @@
|
|||||||
|
// 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;
|
||||||
|
using SharpEmu.Libs.Fiber;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
public static class KernelEventFlagCompatExports
|
||||||
|
{
|
||||||
|
private const int MaxEventFlagNameLength = 31;
|
||||||
|
private const int HostWaitPumpMilliseconds = 1;
|
||||||
|
private const uint AttrThreadFifo = 0x01;
|
||||||
|
private const uint AttrThreadPriority = 0x02;
|
||||||
|
private const uint AttrSingle = 0x10;
|
||||||
|
private const uint AttrMulti = 0x20;
|
||||||
|
private const uint WaitAnd = 0x01;
|
||||||
|
private const uint WaitOr = 0x02;
|
||||||
|
private const uint ClearAll = 0x10;
|
||||||
|
private const uint ClearPattern = 0x20;
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<ulong, EventFlagState> _eventFlags = new();
|
||||||
|
private static long _nextEventFlagHandle = 1;
|
||||||
|
|
||||||
|
private sealed class EventFlagState
|
||||||
|
{
|
||||||
|
public required string Name { get; init; }
|
||||||
|
public required uint Attributes { get; init; }
|
||||||
|
public ulong Bits { get; set; }
|
||||||
|
public int WaitingThreads { get; set; }
|
||||||
|
public object Gate { get; } = new();
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "BpFoboUJoZU",
|
||||||
|
ExportName = "sceKernelCreateEventFlag",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelCreateEventFlag(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var attributes = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
var initialPattern = ctx[CpuRegister.Rcx];
|
||||||
|
var optionAddress = ctx[CpuRegister.R8];
|
||||||
|
|
||||||
|
if (outAddress == 0 ||
|
||||||
|
nameAddress == 0 ||
|
||||||
|
optionAddress != 0 ||
|
||||||
|
!IsValidAttributes(attributes))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
||||||
|
_eventFlags[handle] = new EventFlagState
|
||||||
|
{
|
||||||
|
Name = name,
|
||||||
|
Attributes = attributes,
|
||||||
|
Bits = initialPattern,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||||
|
{
|
||||||
|
_eventFlags.TryRemove(handle, out _);
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8mql9OcQnd4",
|
||||||
|
ExportName = "sceKernelDeleteEventFlag",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelDeleteEventFlag(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
if (!_eventFlags.TryRemove(handle, out var state))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
Monitor.PulseAll(state.Gate);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "IOnSvHzqu6A",
|
||||||
|
ExportName = "sceKernelSetEventFlag",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelSetEventFlag(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var pattern = ctx[CpuRegister.Rsi];
|
||||||
|
var returnRip = GetCurrentReturnRip();
|
||||||
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
state.Bits |= pattern;
|
||||||
|
Monitor.PulseAll(state.Gate);
|
||||||
|
TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "7uhBFWRAS60",
|
||||||
|
ExportName = "sceKernelClearEventFlag",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelClearEventFlag(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var pattern = ctx[CpuRegister.Rsi];
|
||||||
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
state.Bits &= pattern;
|
||||||
|
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "9lvj5DjHZiA",
|
||||||
|
ExportName = "sceKernelPollEventFlag",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelPollEventFlag(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var pattern = ctx[CpuRegister.Rsi];
|
||||||
|
var waitMode = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
var resultAddress = ctx[CpuRegister.Rcx];
|
||||||
|
|
||||||
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
|
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JTvBflhYazQ",
|
||||||
|
ExportName = "sceKernelWaitEventFlag",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelWaitEventFlag(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var pattern = ctx[CpuRegister.Rsi];
|
||||||
|
var waitMode = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
var resultAddress = ctx[CpuRegister.Rcx];
|
||||||
|
var timeoutAddress = ctx[CpuRegister.R8];
|
||||||
|
var returnRip = GetCurrentReturnRip();
|
||||||
|
|
||||||
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
uint timeoutUsec = 0;
|
||||||
|
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Monitor.Enter(state.Gate);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, immediateWaitResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timeoutAddress != 0)
|
||||||
|
{
|
||||||
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
|
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
|
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
|
var managedThread = Environment.CurrentManagedThreadId;
|
||||||
|
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"sceKernelWaitEventFlag",
|
||||||
|
GetEventFlagWakeKey(handle),
|
||||||
|
() => (int)blockedWaitResult,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (!TryPrepareBlockedWait(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
pattern,
|
||||||
|
waitMode,
|
||||||
|
resultAddress,
|
||||||
|
out var preparedResult))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedWaitResult = preparedResult;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
|
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
||||||
|
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||||
|
if (!requestedBlock)
|
||||||
|
{
|
||||||
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
|
if (scheduler is null)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.WaitingThreads++;
|
||||||
|
TraceEventFlag($"wait-pump handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||||
|
var releaseWaiter = true;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
Monitor.Exit(state.Gate);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
scheduler.Pump(ctx, "sceKernelWaitEventFlag");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Monitor.Enter(state.Gate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var pumpedWaitResult))
|
||||||
|
{
|
||||||
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
releaseWaiter = false;
|
||||||
|
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||||
|
return SetReturn(ctx, pumpedWaitResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (releaseWaiter)
|
||||||
|
{
|
||||||
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
state.WaitingThreads++;
|
||||||
|
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Monitor.Exit(state.Gate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "PZku4ZrXJqg",
|
||||||
|
ExportName = "sceKernelCancelEventFlag",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelCancelEventFlag(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var setPattern = ctx[CpuRegister.Rsi];
|
||||||
|
var waiterCountAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
if (waiterCountAddress != 0 &&
|
||||||
|
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.Bits = setPattern;
|
||||||
|
state.WaitingThreads = 0;
|
||||||
|
Monitor.PulseAll(state.Gate);
|
||||||
|
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidAttributes(uint attributes)
|
||||||
|
{
|
||||||
|
var queueMode = attributes & 0x0F;
|
||||||
|
var threadMode = attributes & 0xF0;
|
||||||
|
return (queueMode is 0 or AttrThreadFifo or AttrThreadPriority) &&
|
||||||
|
(threadMode is 0 or AttrSingle or AttrMulti) &&
|
||||||
|
(attributes & ~0x33u) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidWaitMode(uint waitMode)
|
||||||
|
{
|
||||||
|
var condition = waitMode & 0x0F;
|
||||||
|
var clearMode = waitMode & 0xF0;
|
||||||
|
return condition is WaitAnd or WaitOr &&
|
||||||
|
clearMode is 0 or ClearAll or ClearPattern &&
|
||||||
|
(waitMode & ~0x33u) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsSatisfied(ulong bits, ulong pattern, uint waitMode) =>
|
||||||
|
(waitMode & 0x0F) == WaitAnd
|
||||||
|
? (bits & pattern) == pattern
|
||||||
|
: (bits & pattern) != 0;
|
||||||
|
|
||||||
|
private static void ApplyClearMode(EventFlagState state, ulong pattern, uint waitMode)
|
||||||
|
{
|
||||||
|
switch (waitMode & 0xF0)
|
||||||
|
{
|
||||||
|
case ClearAll:
|
||||||
|
state.Bits = 0;
|
||||||
|
break;
|
||||||
|
case ClearPattern:
|
||||||
|
state.Bits &= ~pattern;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCompleteSatisfiedWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
EventFlagState state,
|
||||||
|
ulong pattern,
|
||||||
|
uint waitMode,
|
||||||
|
ulong resultAddress,
|
||||||
|
out OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
result = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
||||||
|
{
|
||||||
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryPrepareBlockedWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
EventFlagState state,
|
||||||
|
ulong pattern,
|
||||||
|
uint waitMode,
|
||||||
|
ulong resultAddress,
|
||||||
|
out OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
result = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
||||||
|
{
|
||||||
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
|
}
|
||||||
|
|
||||||
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
TraceEventFlag(
|
||||||
|
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetEventFlagWakeKey(ulong handle) =>
|
||||||
|
$"event_flag:0x{handle:X16}";
|
||||||
|
|
||||||
|
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
||||||
|
address == 0 || ctx.TryWriteUInt64(address, bits);
|
||||||
|
|
||||||
|
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 TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[1];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = buffer[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||||
|
{
|
||||||
|
var bytes = new byte[capacity];
|
||||||
|
for (var index = 0; index < bytes.Length; index++)
|
||||||
|
{
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[index] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
var value = (int)result;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceEventFlag(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] event_flag.{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ulong GetCurrentReturnRip() =>
|
||||||
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)
|
||||||
|
? frame.ReturnRip
|
||||||
|
: 0UL;
|
||||||
|
|
||||||
|
private static string FormatFrameChain(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Span<ulong> returns = stackalloc ulong[4];
|
||||||
|
var count = 0;
|
||||||
|
var frame = ctx[CpuRegister.Rbp];
|
||||||
|
for (var index = 0; index < returns.Length && frame != 0; index++)
|
||||||
|
{
|
||||||
|
if (!ctx.TryReadUInt64(frame, out var nextFrame) ||
|
||||||
|
!ctx.TryReadUInt64(frame + sizeof(ulong), out var returnAddress))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
returns[count++] = returnAddress;
|
||||||
|
if (nextFrame <= frame)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
frame = nextFrame;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count switch
|
||||||
|
{
|
||||||
|
0 => "none",
|
||||||
|
1 => $"0x{returns[0]:X16}",
|
||||||
|
2 => $"0x{returns[0]:X16},0x{returns[1]:X16}",
|
||||||
|
3 => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16}",
|
||||||
|
_ => $"0x{returns[0]:X16},0x{returns[1]:X16},0x{returns[2]:X16},0x{returns[3]:X16}",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatGuestWaitObject(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var r12 = ctx[CpuRegister.R12];
|
||||||
|
var r13 = ctx[CpuRegister.R13];
|
||||||
|
var objectAddress = r12 != 0
|
||||||
|
? r12
|
||||||
|
: r13 >= 0xA8
|
||||||
|
? r13 - 0xA8
|
||||||
|
: 0;
|
||||||
|
|
||||||
|
var builder = new StringBuilder(256);
|
||||||
|
builder.Append($"r12=0x{r12:X16} r13=0x{r13:X16}");
|
||||||
|
if (objectAddress == 0)
|
||||||
|
{
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
builder.Append($" obj=0x{objectAddress:X16}");
|
||||||
|
AppendUInt32(builder, ctx, objectAddress + 0x58, "o58");
|
||||||
|
AppendUInt32(builder, ctx, objectAddress + 0x5C, "o5C");
|
||||||
|
AppendUInt64(builder, ctx, objectAddress + 0x60, "o60");
|
||||||
|
AppendByte(builder, ctx, objectAddress + 0x6C, "state6C");
|
||||||
|
AppendByte(builder, ctx, objectAddress + 0x6D, "o6D");
|
||||||
|
AppendByte(builder, ctx, objectAddress + 0xA0, "waitA0");
|
||||||
|
AppendByte(builder, ctx, objectAddress + 0xA1, "stateA1");
|
||||||
|
AppendByte(builder, ctx, objectAddress + 0xA2, "oA2");
|
||||||
|
AppendUInt64(builder, ctx, objectAddress + 0xA8, "eventA8");
|
||||||
|
if (r13 != 0)
|
||||||
|
{
|
||||||
|
AppendUInt64(builder, ctx, r13, "r13_0");
|
||||||
|
AppendUInt64(builder, ctx, r13 + 8, "r13_8");
|
||||||
|
}
|
||||||
|
|
||||||
|
return builder.ToString();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
|
{
|
||||||
|
if (TryReadByte(ctx, address, out var value))
|
||||||
|
{
|
||||||
|
builder.Append($" {name}=0x{value:X2}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
|
{
|
||||||
|
if (TryReadUInt32(ctx, address, out var value))
|
||||||
|
{
|
||||||
|
builder.Append($" {name}=0x{value:X8}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
|
{
|
||||||
|
if (TryReadUInt64(ctx, address, out var value))
|
||||||
|
{
|
||||||
|
builder.Append($" {name}=0x{value:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,10 +10,14 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
public static class KernelEventQueueCompatExports
|
public static class KernelEventQueueCompatExports
|
||||||
{
|
{
|
||||||
private const int KernelEventSize = 0x20;
|
private const int KernelEventSize = 0x20;
|
||||||
|
public const short KernelEventFilterGraphics = -14;
|
||||||
|
public const short KernelEventFilterAmpr = -16;
|
||||||
|
public const short KernelEventFilterAmprSystem = -17;
|
||||||
|
|
||||||
private static readonly object _eventQueueGate = new();
|
private static readonly object _eventQueueGate = new();
|
||||||
private static readonly HashSet<ulong> _eventQueues = new();
|
private static readonly HashSet<ulong> _eventQueues = new();
|
||||||
private static readonly Dictionary<ulong, Queue<KernelQueuedEvent>> _pendingEvents = new();
|
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
|
||||||
|
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
|
||||||
private static long _nextEventQueueHandle = 1;
|
private static long _nextEventQueueHandle = 1;
|
||||||
|
|
||||||
public readonly record struct KernelQueuedEvent(
|
public readonly record struct KernelQueuedEvent(
|
||||||
@@ -24,6 +28,11 @@ public static class KernelEventQueueCompatExports
|
|||||||
ulong Data,
|
ulong Data,
|
||||||
ulong UserData);
|
ulong UserData);
|
||||||
|
|
||||||
|
private readonly record struct KernelEventRegistration(
|
||||||
|
ulong Ident,
|
||||||
|
short Filter,
|
||||||
|
ulong UserData);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "D0OdFMjp46I",
|
Nid = "D0OdFMjp46I",
|
||||||
ExportName = "sceKernelCreateEqueue",
|
ExportName = "sceKernelCreateEqueue",
|
||||||
@@ -41,7 +50,8 @@ public static class KernelEventQueueCompatExports
|
|||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
_eventQueues.Add(handle);
|
_eventQueues.Add(handle);
|
||||||
_pendingEvents[handle] = new Queue<KernelQueuedEvent>();
|
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
|
||||||
|
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||||
@@ -65,6 +75,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
{
|
{
|
||||||
_eventQueues.Remove(handle);
|
_eventQueues.Remove(handle);
|
||||||
_pendingEvents.Remove(handle);
|
_pendingEvents.Remove(handle);
|
||||||
|
_registeredEvents.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceEventQueue(ctx, "delete", handle);
|
TraceEventQueue(ctx, "delete", handle);
|
||||||
@@ -122,8 +133,16 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelAddAmprEvent(CpuContext ctx)
|
public static int KernelAddAmprEvent(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "add_ampr", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var registered = RegisterEvent(
|
||||||
|
handle,
|
||||||
|
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||||
|
KernelEventFilterAmpr,
|
||||||
|
ctx[CpuRegister.Rdx]);
|
||||||
|
TraceEventQueue(ctx, "add_ampr", handle);
|
||||||
|
return registered
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -133,8 +152,16 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelAddAmprSystemEvent(CpuContext ctx)
|
public static int KernelAddAmprSystemEvent(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "add_ampr_system", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var registered = RegisterEvent(
|
||||||
|
handle,
|
||||||
|
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||||
|
KernelEventFilterAmprSystem,
|
||||||
|
ctx[CpuRegister.Rdx]);
|
||||||
|
TraceEventQueue(ctx, "add_ampr_system", handle);
|
||||||
|
return registered
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -144,8 +171,15 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelDeleteAmprEvent(CpuContext ctx)
|
public static int KernelDeleteAmprEvent(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "delete_ampr", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var deleted = DeleteRegisteredEvent(
|
||||||
|
handle,
|
||||||
|
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||||
|
KernelEventFilterAmpr);
|
||||||
|
TraceEventQueue(ctx, "delete_ampr", handle);
|
||||||
|
return deleted
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -155,8 +189,15 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelDeleteAmprSystemEvent(CpuContext ctx)
|
public static int KernelDeleteAmprSystemEvent(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "delete_ampr_system", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var deleted = DeleteRegisteredEvent(
|
||||||
|
handle,
|
||||||
|
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||||
|
KernelEventFilterAmprSystem);
|
||||||
|
TraceEventQueue(ctx, "delete_ampr_system", handle);
|
||||||
|
return deleted
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -171,6 +212,57 @@ public static class KernelEventQueueCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "vz+pg2zdopI",
|
||||||
|
ExportName = "sceKernelGetEventUserData",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelGetEventUserData(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x18, out var userData);
|
||||||
|
ctx[CpuRegister.Rax] = userData;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mJ7aghmgvfc",
|
||||||
|
ExportName = "sceKernelGetEventId",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelGetEventId(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi], out var ident);
|
||||||
|
ctx[CpuRegister.Rax] = ident;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "23CPPI1tyBY",
|
||||||
|
ExportName = "sceKernelGetEventFilter",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelGetEventFilter(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Span<byte> filterBytes = stackalloc byte[sizeof(short)];
|
||||||
|
var filter = ctx.Memory.TryRead(ctx[CpuRegister.Rdi] + 0x08, filterBytes)
|
||||||
|
? BinaryPrimitives.ReadInt16LittleEndian(filterBytes)
|
||||||
|
: (short)0;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((uint)filter);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "kwGyyjohI50",
|
||||||
|
ExportName = "sceKernelGetEventData",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelGetEventData(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x10, out var data);
|
||||||
|
ctx[CpuRegister.Rax] = data;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "fzyMKs9kim0",
|
Nid = "fzyMKs9kim0",
|
||||||
ExportName = "sceKernelWaitEqueue",
|
ExportName = "sceKernelWaitEqueue",
|
||||||
@@ -196,7 +288,13 @@ public static class KernelEventQueueCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue"))
|
if (timeoutAddress == 0 &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"sceKernelWaitEqueue",
|
||||||
|
GetEventQueueWakeKey(handle),
|
||||||
|
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress),
|
||||||
|
() => HasPendingEvents(handle)))
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "wait-block", handle);
|
TraceEventQueue(ctx, "wait-block", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -216,6 +314,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
|
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
|
||||||
{
|
{
|
||||||
|
var queued = false;
|
||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
if (!_eventQueues.Contains(handle))
|
if (!_eventQueues.Contains(handle))
|
||||||
@@ -225,15 +324,228 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
{
|
{
|
||||||
queue = new Queue<KernelQueuedEvent>();
|
queue = new LinkedList<KernelQueuedEvent>();
|
||||||
_pendingEvents[handle] = queue;
|
_pendingEvents[handle] = queue;
|
||||||
}
|
}
|
||||||
|
|
||||||
queue.Enqueue(queuedEvent);
|
queue.AddLast(queuedEvent);
|
||||||
|
queued = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queued)
|
||||||
|
{
|
||||||
|
WakeEventQueue(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool RegisterEvent(
|
||||||
|
ulong handle,
|
||||||
|
ulong ident,
|
||||||
|
short filter,
|
||||||
|
ulong userData)
|
||||||
|
{
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
if (!_eventQueues.Contains(handle))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_registeredEvents.TryGetValue(handle, out var events))
|
||||||
|
{
|
||||||
|
events = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||||
|
_registeredEvents[handle] = events;
|
||||||
|
}
|
||||||
|
|
||||||
|
events[(ident, filter)] = new KernelEventRegistration(ident, filter, userData);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool DeleteRegisteredEvent(
|
||||||
|
ulong handle,
|
||||||
|
ulong ident,
|
||||||
|
short filter)
|
||||||
|
{
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
return _registeredEvents.TryGetValue(handle, out var events) &&
|
||||||
|
events.Remove((ident, filter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int TriggerRegisteredEvents(
|
||||||
|
ulong ident,
|
||||||
|
short filter,
|
||||||
|
ulong data)
|
||||||
|
{
|
||||||
|
List<ulong>? wakeHandles = null;
|
||||||
|
var triggeredCount = 0;
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
foreach (var (handle, registrations) in _registeredEvents)
|
||||||
|
{
|
||||||
|
if (!registrations.TryGetValue((ident, filter), out var registration))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
|
{
|
||||||
|
queue = new LinkedList<KernelQueuedEvent>();
|
||||||
|
_pendingEvents[handle] = queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueOrUpdateEvent(
|
||||||
|
queue,
|
||||||
|
new KernelQueuedEvent(
|
||||||
|
registration.Ident,
|
||||||
|
registration.Filter,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
data,
|
||||||
|
registration.UserData));
|
||||||
|
(wakeHandles ??= new List<ulong>()).Add(handle);
|
||||||
|
triggeredCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wakeHandles is not null)
|
||||||
|
{
|
||||||
|
foreach (var handle in wakeHandles)
|
||||||
|
{
|
||||||
|
WakeEventQueue(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggeredCount;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TriggerDisplayEvent(
|
||||||
|
ulong handle,
|
||||||
|
ulong ident,
|
||||||
|
short filter,
|
||||||
|
ulong eventHint,
|
||||||
|
ulong userData)
|
||||||
|
{
|
||||||
|
var triggered = false;
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
if (!_eventQueues.Contains(handle))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_pendingEvents.TryGetValue(handle, out var events))
|
||||||
|
{
|
||||||
|
events = new LinkedList<KernelQueuedEvent>();
|
||||||
|
_pendingEvents[handle] = events;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = 1UL;
|
||||||
|
var pendingNode = FindPendingEvent(events, ident, filter);
|
||||||
|
if (pendingNode is not null)
|
||||||
|
{
|
||||||
|
count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL);
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeBits = unchecked((ulong)Environment.TickCount64) & 0xFFFUL;
|
||||||
|
var eventData = timeBits | (count << 12) | (eventHint & 0xFFFF_FFFF_FFFF_0000UL);
|
||||||
|
var triggeredEvent = new KernelQueuedEvent(
|
||||||
|
ident,
|
||||||
|
filter,
|
||||||
|
0x20,
|
||||||
|
0,
|
||||||
|
eventData,
|
||||||
|
userData);
|
||||||
|
|
||||||
|
if (pendingNode is not null)
|
||||||
|
{
|
||||||
|
pendingNode.Value = triggeredEvent;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
events.AddLast(triggeredEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
triggered = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (triggered)
|
||||||
|
{
|
||||||
|
WakeEventQueue(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ResumeWaitEqueue(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong handle,
|
||||||
|
ulong eventsAddress,
|
||||||
|
int eventCapacity,
|
||||||
|
ulong outCountAddress)
|
||||||
|
{
|
||||||
|
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasPendingEvents(ulong handle)
|
||||||
|
{
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
return _pendingEvents.TryGetValue(handle, out var events) && events.Count != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void QueueOrUpdateEvent(
|
||||||
|
LinkedList<KernelQueuedEvent> queue,
|
||||||
|
KernelQueuedEvent queuedEvent)
|
||||||
|
{
|
||||||
|
var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter);
|
||||||
|
if (pendingNode is null)
|
||||||
|
{
|
||||||
|
queue.AddLast(queuedEvent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingNode.Value = queuedEvent with
|
||||||
|
{
|
||||||
|
Fflags = Math.Max(pendingNode.Value.Fflags + 1, queuedEvent.Fflags),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LinkedListNode<KernelQueuedEvent>? FindPendingEvent(
|
||||||
|
LinkedList<KernelQueuedEvent> queue,
|
||||||
|
ulong ident,
|
||||||
|
short filter)
|
||||||
|
{
|
||||||
|
for (var node = queue.First; node is not null; node = node.Next)
|
||||||
|
{
|
||||||
|
if (node.Value.Ident == ident && node.Value.Filter == filter)
|
||||||
|
{
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetEventQueueWakeKey(ulong handle) =>
|
||||||
|
$"sceKernelWaitEqueue:{handle:X16}";
|
||||||
|
|
||||||
|
private static void WakeEventQueue(ulong handle)
|
||||||
|
{
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventQueueWakeKey(handle));
|
||||||
|
}
|
||||||
|
|
||||||
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
|
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
|
||||||
{
|
{
|
||||||
if (eventsAddress == 0 || eventCapacity <= 0)
|
if (eventsAddress == 0 || eventCapacity <= 0)
|
||||||
@@ -253,7 +565,8 @@ public static class KernelEventQueueCompatExports
|
|||||||
events = new KernelQueuedEvent[count];
|
events = new KernelQueuedEvent[count];
|
||||||
for (var i = 0; i < count; i++)
|
for (var i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
events[i] = queue.Dequeue();
|
events[i] = queue.First!.Value;
|
||||||
|
queue.RemoveFirst();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -204,6 +204,16 @@ public static class KernelExports
|
|||||||
var nameAddress = ctx[CpuRegister.R8];
|
var nameAddress = ctx[CpuRegister.R8];
|
||||||
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
|
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
|
||||||
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
|
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
|
||||||
|
KernelPthreadExtendedCompatExports.GetThreadStartScheduling(
|
||||||
|
ctx,
|
||||||
|
attrAddress,
|
||||||
|
out var priority,
|
||||||
|
out var affinityMask);
|
||||||
|
KernelPthreadExtendedCompatExports.RegisterThreadStart(
|
||||||
|
threadHandle,
|
||||||
|
name,
|
||||||
|
priority,
|
||||||
|
affinityMask);
|
||||||
if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, threadHandle))
|
if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, threadHandle))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
@@ -212,13 +222,22 @@ public static class KernelExports
|
|||||||
if (ShouldTracePthread())
|
if (ShouldTracePthread())
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} name='{name}' -> thread=0x{threadHandle:X16}");
|
$"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} " +
|
||||||
|
$"entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} " +
|
||||||
|
$"name='{name}' priority={priority} affinity=0x{affinityMask:X} -> thread=0x{threadHandle:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var scheduler = GuestThreadExecution.Scheduler;
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
if (scheduler is not null && entryAddress != 0)
|
if (scheduler is not null && entryAddress != 0)
|
||||||
{
|
{
|
||||||
var request = new GuestThreadStartRequest(threadHandle, entryAddress, argument, attrAddress, name);
|
var request = new GuestThreadStartRequest(
|
||||||
|
threadHandle,
|
||||||
|
entryAddress,
|
||||||
|
argument,
|
||||||
|
attrAddress,
|
||||||
|
name,
|
||||||
|
priority,
|
||||||
|
affinityMask);
|
||||||
if (!scheduler.TryStartThread(ctx, request, out var error))
|
if (!scheduler.TryStartThread(ctx, request, out var error))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -252,6 +271,32 @@ public static class KernelExports
|
|||||||
return PthreadCreate(ctx);
|
return PthreadCreate(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3kg7rT0NQIs",
|
||||||
|
ExportName = "scePthreadExit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadExit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var value = ctx[CpuRegister.Rdi];
|
||||||
|
GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value);
|
||||||
|
ctx[CpuRegister.Rax] = value;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "FJrT5LuUBAU",
|
||||||
|
ExportName = "pthread_exit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePosix")]
|
||||||
|
public static int PosixPthreadExit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var value = ctx[CpuRegister.Rdi];
|
||||||
|
GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value);
|
||||||
|
ctx[CpuRegister.Rax] = value;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "onNY9Byn-W8",
|
Nid = "onNY9Byn-W8",
|
||||||
ExportName = "scePthreadJoin",
|
ExportName = "scePthreadJoin",
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
@@ -9,25 +11,33 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
|
|
||||||
public static class KernelPthreadCompatExports
|
public static class KernelPthreadCompatExports
|
||||||
{
|
{
|
||||||
private const int MutexTypeDefault = 1;
|
private const int MutexTypeDefault = 0;
|
||||||
private const int MutexTypeErrorCheck = 1;
|
private const int MutexTypeErrorCheck = 1;
|
||||||
private const int MutexTypeRecursive = 2;
|
private const int MutexTypeRecursive = 2;
|
||||||
private const int MutexTypeNormal = 3;
|
private const int MutexTypeNormal = 3;
|
||||||
private const int MutexTypeAdaptiveNp = 4;
|
private const int MutexTypeAdaptiveNp = 4;
|
||||||
private const ulong StaticAdaptiveMutexInitializer = 1;
|
private const ulong StaticAdaptiveMutexInitializer = 1;
|
||||||
private const ulong SyntheticMutexHandleBase = 0x00006000_0000_0000;
|
private const int MutexObjectSize = 0x100;
|
||||||
private const ulong SyntheticMutexAttrHandleBase = 0x00006001_0000_0000;
|
private const int MutexAttrObjectSize = 0x40;
|
||||||
private const ulong SyntheticCondHandleBase = 0x00006002_0000_0000;
|
private const int CondObjectSize = 0x100;
|
||||||
private const int DefaultSpuriousCondWakeMilliseconds = 1;
|
private const int DefaultSpuriousCondWakeMilliseconds = 1;
|
||||||
|
private const int PthreadOnceUninitialized = 0;
|
||||||
|
private const int PthreadOnceInProgress = 1;
|
||||||
|
private const int PthreadOnceDone = 2;
|
||||||
|
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly Dictionary<ulong, PthreadMutexState> _mutexStates = new();
|
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
|
||||||
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
|
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
|
||||||
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
||||||
|
private static readonly Dictionary<ulong, object> _onceGates = new();
|
||||||
private static readonly HashSet<ulong> _condAttrStates = new();
|
private static readonly HashSet<ulong> _condAttrStates = new();
|
||||||
private static long _nextSyntheticMutexHandleId = 1;
|
private static readonly bool _tracePthreads =
|
||||||
private static long _nextSyntheticMutexAttrHandleId = 1;
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||||
private static long _nextSyntheticCondHandleId = 1;
|
private static readonly bool _tracePthreadConds =
|
||||||
|
_tracePthreads ||
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
|
||||||
|
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
|
||||||
|
|
||||||
private sealed class PthreadMutexState
|
private sealed class PthreadMutexState
|
||||||
{
|
{
|
||||||
@@ -38,6 +48,12 @@ public static class KernelPthreadCompatExports
|
|||||||
public int Protocol { get; set; }
|
public int Protocol { get; set; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class PthreadMutexWaiter
|
||||||
|
{
|
||||||
|
public required ulong ThreadId { get; init; }
|
||||||
|
public int Reserved;
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class PthreadCondState
|
private sealed class PthreadCondState
|
||||||
{
|
{
|
||||||
public object SyncRoot { get; } = new();
|
public object SyncRoot { get; } = new();
|
||||||
@@ -60,6 +76,13 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "EotR8a3ASf4",
|
||||||
|
ExportName = "pthread_self",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadSelf(CpuContext ctx) => PthreadSelf(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "3PtV6p3QNX4",
|
Nid = "3PtV6p3QNX4",
|
||||||
ExportName = "scePthreadEqual",
|
ExportName = "scePthreadEqual",
|
||||||
@@ -343,6 +366,93 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "14bOACANTBo",
|
||||||
|
ExportName = "scePthreadOnce",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadOnce(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var onceAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var initRoutine = ctx[CpuRegister.Rsi];
|
||||||
|
if (onceAddress == 0 || initRoutine == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadInt32(ctx, onceAddress, out var onceValue))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onceValue == PthreadOnceDone)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
var gate = GetPthreadOnceGate(onceAddress);
|
||||||
|
var shouldCall = false;
|
||||||
|
lock (gate)
|
||||||
|
{
|
||||||
|
if (!TryReadInt32(ctx, onceAddress, out onceValue))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (onceValue == PthreadOnceInProgress)
|
||||||
|
{
|
||||||
|
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
|
||||||
|
if (!TryReadInt32(ctx, onceAddress, out onceValue))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (onceValue != PthreadOnceDone)
|
||||||
|
{
|
||||||
|
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceInProgress))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
shouldCall = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (shouldCall)
|
||||||
|
{
|
||||||
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
|
string? error = null;
|
||||||
|
if (scheduler is null ||
|
||||||
|
!scheduler.TryCallGuestFunction(ctx, initRoutine, 0, 0, 0, 0, "pthread_once", out error))
|
||||||
|
{
|
||||||
|
lock (gate)
|
||||||
|
{
|
||||||
|
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||||
|
Monitor.PulseAll(gate);
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePthreadOnce(onceAddress, initRoutine, "failed", error);
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (gate)
|
||||||
|
{
|
||||||
|
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceDone))
|
||||||
|
{
|
||||||
|
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||||
|
Monitor.PulseAll(gate);
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Monitor.PulseAll(gate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null);
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||||
{
|
{
|
||||||
if (mutexAddress == 0)
|
if (mutexAddress == 0)
|
||||||
@@ -357,14 +467,28 @@ public static class KernelPthreadCompatExports
|
|||||||
Protocol = attr.Protocol,
|
Protocol = attr.Protocol,
|
||||||
};
|
};
|
||||||
|
|
||||||
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexHandleBase, ref _nextSyntheticMutexHandleId);
|
if (!TryAllocateOpaqueObject(ctx, MutexObjectSize, out var handle))
|
||||||
lock (_stateGate)
|
|
||||||
{
|
{
|
||||||
_mutexStates[mutexAddress] = state;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
_mutexStates[syntheticHandle] = state;
|
}
|
||||||
|
if (!InitializeMutexObject(ctx, handle, state))
|
||||||
|
{
|
||||||
|
state.Semaphore.Dispose();
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
_mutexStates[mutexAddress] = state;
|
||||||
|
_mutexStates[handle] = state;
|
||||||
|
|
||||||
|
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, handle))
|
||||||
|
{
|
||||||
|
_mutexStates.TryRemove(mutexAddress, out _);
|
||||||
|
_mutexStates.TryRemove(handle, out _);
|
||||||
|
|
||||||
|
state.Semaphore.Dispose();
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt64(mutexAddress, syntheticHandle);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -376,15 +500,10 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress);
|
var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress);
|
||||||
PthreadMutexState? state;
|
_mutexStates.TryRemove(resolvedAddress, out var state);
|
||||||
lock (_stateGate)
|
if (resolvedAddress != mutexAddress)
|
||||||
{
|
{
|
||||||
_mutexStates.TryGetValue(resolvedAddress, out state);
|
_mutexStates.TryRemove(mutexAddress, out _);
|
||||||
_mutexStates.Remove(resolvedAddress);
|
|
||||||
if (resolvedAddress != mutexAddress)
|
|
||||||
{
|
|
||||||
_mutexStates.Remove(mutexAddress);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state is null)
|
if (state is null)
|
||||||
@@ -392,7 +511,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt64(mutexAddress, 0);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
||||||
state.Semaphore.Dispose();
|
state.Semaphore.Dispose();
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -422,7 +541,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state.Type is MutexTypeNormal or MutexTypeAdaptiveNp)
|
if (state.Type is MutexTypeDefault or MutexTypeNormal or MutexTypeAdaptiveNp)
|
||||||
{
|
{
|
||||||
if (tryOnly)
|
if (tryOnly)
|
||||||
{
|
{
|
||||||
@@ -447,15 +566,33 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var acquired = true;
|
var acquired = state.Semaphore.Wait(0);
|
||||||
if (tryOnly)
|
if (!acquired)
|
||||||
{
|
{
|
||||||
acquired = state.Semaphore.Wait(0);
|
// Guest-thread blocking for pthread_mutex_lock is currently disabled.
|
||||||
}
|
// Demon's Souls deadlocks during PS5SyncEvent initialization.
|
||||||
else
|
/* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||||
{
|
if (!tryOnly &&
|
||||||
state.Semaphore.Wait();
|
GuestThreadExecution.IsGuestThread &&
|
||||||
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"pthread_mutex_lock",
|
||||||
|
GetMutexWakeKey(resolvedAddress),
|
||||||
|
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||||
|
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
} */
|
||||||
|
|
||||||
|
if (!tryOnly)
|
||||||
|
{
|
||||||
|
state.Semaphore.Wait();
|
||||||
|
acquired = true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!acquired)
|
if (!acquired)
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
@@ -514,6 +651,7 @@ public static class KernelPthreadCompatExports
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
state.Semaphore.Release();
|
state.Semaphore.Release();
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
|
||||||
}
|
}
|
||||||
catch (SemaphoreFullException)
|
catch (SemaphoreFullException)
|
||||||
{
|
{
|
||||||
@@ -533,14 +671,34 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexAttrHandleBase, ref _nextSyntheticMutexAttrHandleId);
|
if (!TryAllocateOpaqueObject(ctx, MutexAttrObjectSize, out var handle))
|
||||||
lock (_stateGate)
|
|
||||||
{
|
{
|
||||||
_mutexAttrStates[attrAddress] = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
_mutexAttrStates[syntheticHandle] = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
}
|
||||||
|
|
||||||
|
var initialState = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||||
|
if (!WriteMutexAttrObject(ctx, handle, initialState))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_mutexAttrStates[attrAddress] = initialState;
|
||||||
|
_mutexAttrStates[handle] = initialState;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, handle))
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_mutexAttrStates.Remove(attrAddress);
|
||||||
|
_mutexAttrStates.Remove(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt64(attrAddress, syntheticHandle);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -572,6 +730,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
|
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
|
||||||
|
PthreadMutexAttrState updatedState;
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
|
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
|
||||||
@@ -579,14 +738,17 @@ public static class KernelPthreadCompatExports
|
|||||||
state = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
state = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
_mutexAttrStates[resolvedAddress] = state with { Type = NormalizeMutexType(type) };
|
updatedState = state with { Type = NormalizeMutexType(type) };
|
||||||
|
_mutexAttrStates[resolvedAddress] = updatedState;
|
||||||
if (resolvedAddress != attrAddress)
|
if (resolvedAddress != attrAddress)
|
||||||
{
|
{
|
||||||
_mutexAttrStates[attrAddress] = _mutexAttrStates[resolvedAddress];
|
_mutexAttrStates[attrAddress] = updatedState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return WriteMutexAttrObject(ctx, resolvedAddress, updatedState)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int PthreadMutexattrSetprotocolCore(CpuContext ctx, ulong attrAddress, int protocol)
|
private static int PthreadMutexattrSetprotocolCore(CpuContext ctx, ulong attrAddress, int protocol)
|
||||||
@@ -597,6 +759,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
|
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
|
||||||
|
PthreadMutexAttrState updatedState;
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
|
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
|
||||||
@@ -604,14 +767,17 @@ public static class KernelPthreadCompatExports
|
|||||||
state = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
state = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
_mutexAttrStates[resolvedAddress] = state with { Protocol = protocol };
|
updatedState = state with { Protocol = protocol };
|
||||||
|
_mutexAttrStates[resolvedAddress] = updatedState;
|
||||||
if (resolvedAddress != attrAddress)
|
if (resolvedAddress != attrAddress)
|
||||||
{
|
{
|
||||||
_mutexAttrStates[attrAddress] = _mutexAttrStates[resolvedAddress];
|
_mutexAttrStates[attrAddress] = updatedState;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return WriteMutexAttrObject(ctx, resolvedAddress, updatedState)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong ResolveMutexHandle(CpuContext ctx, ulong mutexAddress)
|
private static ulong ResolveMutexHandle(CpuContext ctx, ulong mutexAddress)
|
||||||
@@ -621,22 +787,16 @@ public static class KernelPthreadCompatExports
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_stateGate)
|
if (_mutexStates.ContainsKey(mutexAddress))
|
||||||
{
|
{
|
||||||
if (_mutexStates.ContainsKey(mutexAddress))
|
return mutexAddress;
|
||||||
{
|
|
||||||
return mutexAddress;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.TryReadUInt64(mutexAddress, out var pointedHandle) && pointedHandle != 0)
|
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle) && pointedHandle != 0)
|
||||||
{
|
{
|
||||||
lock (_stateGate)
|
if (_mutexStates.ContainsKey(pointedHandle))
|
||||||
{
|
{
|
||||||
if (_mutexStates.ContainsKey(pointedHandle))
|
return pointedHandle;
|
||||||
{
|
|
||||||
return pointedHandle;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -652,16 +812,13 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_stateGate)
|
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
||||||
{
|
{
|
||||||
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
resolvedAddress = mutexAddress;
|
||||||
{
|
return true;
|
||||||
resolvedAddress = mutexAddress;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt64(mutexAddress, out var pointedHandle))
|
if (!KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -673,14 +830,11 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
if (pointedHandle != 0)
|
if (pointedHandle != 0)
|
||||||
{
|
{
|
||||||
lock (_stateGate)
|
if (_mutexStates.TryGetValue(pointedHandle, out state))
|
||||||
{
|
{
|
||||||
if (_mutexStates.TryGetValue(pointedHandle, out state))
|
_mutexStates.TryAdd(mutexAddress, state);
|
||||||
{
|
resolvedAddress = pointedHandle;
|
||||||
_mutexStates[mutexAddress] = state;
|
return true;
|
||||||
resolvedAddress = pointedHandle;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
resolvedAddress = pointedHandle;
|
resolvedAddress = pointedHandle;
|
||||||
@@ -703,15 +857,7 @@ public static class KernelPthreadCompatExports
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_stateGate)
|
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, attrAddress, out var pointedHandle) && pointedHandle != 0)
|
||||||
{
|
|
||||||
if (_mutexAttrStates.ContainsKey(attrAddress))
|
|
||||||
{
|
|
||||||
return attrAddress;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ctx.TryReadUInt64(attrAddress, out var pointedHandle) && pointedHandle != 0)
|
|
||||||
{
|
{
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
@@ -722,6 +868,14 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (_mutexAttrStates.ContainsKey(attrAddress))
|
||||||
|
{
|
||||||
|
return attrAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return attrAddress;
|
return attrAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -756,7 +910,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.TryReadUInt64(condAddress, out var pointedHandle) && pointedHandle != 0)
|
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, condAddress, out var pointedHandle) && pointedHandle != 0)
|
||||||
{
|
{
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
@@ -788,7 +942,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx is null || !ctx.TryReadUInt64(condAddress, out var pointedHandle))
|
if (ctx is null || !KernelMemoryCompatExports.TryReadUInt64Compat(ctx, condAddress, out var pointedHandle))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -816,23 +970,60 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var createdState = new PthreadCondState();
|
var createdState = new PthreadCondState();
|
||||||
var syntheticHandle = AllocateSyntheticHandle(SyntheticCondHandleBase, ref _nextSyntheticCondHandleId);
|
if (!TryAllocateOpaqueObject(ctx, CondObjectSize, out var handle))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
_condStates[condAddress] = createdState;
|
_condStates[condAddress] = createdState;
|
||||||
_condStates[syntheticHandle] = createdState;
|
_condStates[handle] = createdState;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt64(condAddress, syntheticHandle);
|
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, handle))
|
||||||
resolvedAddress = syntheticHandle;
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_condStates.Remove(condAddress);
|
||||||
|
_condStates.Remove(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedAddress = handle;
|
||||||
state = createdState;
|
state = createdState;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong AllocateSyntheticHandle(ulong baseAddress, ref long nextId)
|
private static bool TryAllocateOpaqueObject(CpuContext ctx, int size, out ulong address)
|
||||||
{
|
{
|
||||||
var id = unchecked((ulong)Interlocked.Increment(ref nextId));
|
address = 0;
|
||||||
return baseAddress + (id << 4);
|
if (ctx.Memory is not IGuestMemoryAllocator allocator ||
|
||||||
|
!allocator.TryAllocateGuestMemory((ulong)size, alignment: 0x10, out address))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> initialData = stackalloc byte[size];
|
||||||
|
initialData.Clear();
|
||||||
|
return ctx.Memory.TryWrite(address, initialData);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
||||||
|
TryWriteUInt32(ctx, address + 0x20, unchecked((uint)state.Type)) &&
|
||||||
|
TryWriteUInt32(ctx, address + 0x3C, unchecked((uint)state.Protocol));
|
||||||
|
|
||||||
|
private static bool WriteMutexAttrObject(CpuContext ctx, ulong address, PthreadMutexAttrState state) =>
|
||||||
|
TryWriteUInt32(ctx, address, unchecked((uint)state.Type)) &&
|
||||||
|
TryWriteUInt32(ctx, address + 4, unchecked((uint)state.Protocol));
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
BitConverter.TryWriteBytes(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int PthreadCondInitCore(CpuContext ctx, ulong condAddress)
|
private static int PthreadCondInitCore(CpuContext ctx, ulong condAddress)
|
||||||
@@ -842,15 +1033,29 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var syntheticHandle = AllocateSyntheticHandle(SyntheticCondHandleBase, ref _nextSyntheticCondHandleId);
|
if (!TryAllocateOpaqueObject(ctx, CondObjectSize, out var handle))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
var state = new PthreadCondState();
|
var state = new PthreadCondState();
|
||||||
_condStates[condAddress] = state;
|
_condStates[condAddress] = state;
|
||||||
_condStates[syntheticHandle] = state;
|
_condStates[handle] = state;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, handle))
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_condStates.Remove(condAddress);
|
||||||
|
_condStates.Remove(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt64(condAddress, syntheticHandle);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -871,7 +1076,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt64(condAddress, 0);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1007,6 +1212,63 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
|
||||||
|
$"pthread_mutex:0x{resolvedMutexAddress:X16}";
|
||||||
|
|
||||||
|
private static bool TryReserveBlockedMutexLock(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong mutexAddress,
|
||||||
|
ulong resolvedAddress,
|
||||||
|
PthreadMutexState state,
|
||||||
|
PthreadMutexWaiter waiter)
|
||||||
|
{
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
if (state.OwnerThreadId != 0 || state.RecursionCount != 0)
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "lock-reserve-busy", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
state.OwnerThreadId = waiter.ThreadId;
|
||||||
|
state.RecursionCount = 1;
|
||||||
|
Interlocked.Exchange(ref waiter.Reserved, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePthreadMutex(ctx, "lock-reserve", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int CompleteBlockedMutexLock(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong mutexAddress,
|
||||||
|
ulong resolvedAddress,
|
||||||
|
PthreadMutexState state,
|
||||||
|
PthreadMutexWaiter waiter)
|
||||||
|
{
|
||||||
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
|
if (Interlocked.Exchange(ref waiter.Reserved, 0) == 1)
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "lock-resume", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!state.Semaphore.Wait(0))
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "lock-resume-busy", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
state.OwnerThreadId = currentThreadId;
|
||||||
|
state.RecursionCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePthreadMutex(ctx, "lock-resume", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
|
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
|
||||||
{
|
{
|
||||||
if (timeoutUsec == 0)
|
if (timeoutUsec == 0)
|
||||||
@@ -1040,6 +1302,46 @@ public static class KernelPthreadCompatExports
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static object GetPthreadOnceGate(ulong onceAddress)
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_onceGates.TryGetValue(onceAddress, out var gate))
|
||||||
|
{
|
||||||
|
gate = new object();
|
||||||
|
_onceGates[onceAddress] = gate;
|
||||||
|
}
|
||||||
|
|
||||||
|
return gate;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
|
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
|
||||||
{
|
{
|
||||||
var createdState = new PthreadMutexState
|
var createdState = new PthreadMutexState
|
||||||
@@ -1047,7 +1349,19 @@ public static class KernelPthreadCompatExports
|
|||||||
Type = type,
|
Type = type,
|
||||||
};
|
};
|
||||||
|
|
||||||
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexHandleBase, ref _nextSyntheticMutexHandleId);
|
if (!TryAllocateOpaqueObject(ctx, MutexObjectSize, out var handle))
|
||||||
|
{
|
||||||
|
resolvedAddress = 0;
|
||||||
|
state = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (!InitializeMutexObject(ctx, handle, createdState))
|
||||||
|
{
|
||||||
|
resolvedAddress = 0;
|
||||||
|
state = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
||||||
@@ -1056,18 +1370,27 @@ public static class KernelPthreadCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_mutexStates.TryGetValue(syntheticHandle, out state))
|
if (_mutexStates.TryGetValue(handle, out state))
|
||||||
{
|
{
|
||||||
resolvedAddress = syntheticHandle;
|
resolvedAddress = handle;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
_mutexStates[mutexAddress] = createdState;
|
_mutexStates[mutexAddress] = createdState;
|
||||||
_mutexStates[syntheticHandle] = createdState;
|
_mutexStates[handle] = createdState;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt64(mutexAddress, syntheticHandle);
|
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, handle))
|
||||||
resolvedAddress = syntheticHandle;
|
{
|
||||||
|
_mutexStates.TryRemove(mutexAddress, out _);
|
||||||
|
_mutexStates.TryRemove(handle, out _);
|
||||||
|
|
||||||
|
resolvedAddress = 0;
|
||||||
|
state = null;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
resolvedAddress = handle;
|
||||||
state = createdState;
|
state = createdState;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
@@ -1084,15 +1407,27 @@ public static class KernelPthreadCompatExports
|
|||||||
$"[LOADER][TRACE] pthread_self: stale_rdi=0x{ctx[CpuRegister.Rdi]:X16} thread=0x{currentThreadHandle:X16} tid=0x{currentThreadId:X16}");
|
$"[LOADER][TRACE] pthread_self: stale_rdi=0x{ctx[CpuRegister.Rdi]:X16} thread=0x{currentThreadHandle:X16} tid=0x{currentThreadId:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TracePthreadMutex(CpuContext ctx, string operation, ulong mutexAddress, ulong resolvedAddress, PthreadMutexState? state, ulong currentThreadId, int result)
|
private static void TracePthreadOnce(ulong onceAddress, ulong initRoutine, string operation, string? error)
|
||||||
{
|
{
|
||||||
if (!ShouldTracePthread())
|
if (!ShouldTracePthread())
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryReadUInt64(mutexAddress, out var guestWord0);
|
var suffix = string.IsNullOrWhiteSpace(error) ? string.Empty : $" error={error}";
|
||||||
_ = ctx.TryReadUInt64(mutexAddress + 8, out var guestWord1);
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] pthread_once_{operation}: once=0x{onceAddress:X16} init=0x{initRoutine:X16}{suffix}");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TracePthreadMutex(CpuContext ctx, string operation, ulong mutexAddress, ulong resolvedAddress, PthreadMutexState? state, ulong currentThreadId, int result)
|
||||||
|
{
|
||||||
|
if (!ShouldTracePthreadMutex(mutexAddress, resolvedAddress))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var guestWord0);
|
||||||
|
_ = KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress + 8, out var guestWord1);
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
||||||
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
|
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
|
||||||
@@ -1102,7 +1437,7 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
|
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
|
||||||
{
|
{
|
||||||
if (!ShouldTracePthread())
|
if (!_tracePthreadConds)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -1114,6 +1449,45 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
private static bool ShouldTracePthread()
|
private static bool ShouldTracePthread()
|
||||||
{
|
{
|
||||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
return _tracePthreads;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTracePthreadMutex(ulong mutexAddress, ulong resolvedAddress)
|
||||||
|
{
|
||||||
|
if (_tracePthreadMutexFilter is null || _tracePthreadMutexFilter.Count == 0)
|
||||||
|
{
|
||||||
|
return _tracePthreads;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _tracePthreadMutexFilter.Contains(mutexAddress) ||
|
||||||
|
_tracePthreadMutexFilter.Contains(resolvedAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static HashSet<ulong>? ParseTraceAddressFilter(string? filter)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(filter))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var addresses = new HashSet<ulong>();
|
||||||
|
foreach (var token in filter.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||||
|
{
|
||||||
|
var normalized = token.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||||
|
? token[2..]
|
||||||
|
: token;
|
||||||
|
normalized = normalized.TrimStart('0');
|
||||||
|
|
||||||
|
if (ulong.TryParse(
|
||||||
|
normalized.Length == 0 ? "0" : normalized,
|
||||||
|
System.Globalization.NumberStyles.HexNumber,
|
||||||
|
System.Globalization.CultureInfo.InvariantCulture,
|
||||||
|
out var address))
|
||||||
|
{
|
||||||
|
addresses.Add(address);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return addresses.Count == 0 ? null : addresses;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
@@ -12,13 +13,13 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
public static class KernelPthreadExtendedCompatExports
|
public static class KernelPthreadExtendedCompatExports
|
||||||
{
|
{
|
||||||
private const int DefaultThreadPriority = 700;
|
private const int DefaultThreadPriority = 700;
|
||||||
private const ulong DefaultThreadAffinityMask = ulong.MaxValue;
|
private const ulong DefaultThreadAffinityMask = 0x7FUL;
|
||||||
private const int DefaultDetachState = 0;
|
private const int DefaultDetachState = 0;
|
||||||
private const ulong DefaultGuardSize = 0x1000UL;
|
private const ulong DefaultGuardSize = 0x1000UL;
|
||||||
private const ulong DefaultStackSize = 0x1_00000UL;
|
private const ulong DefaultStackSize = 0x1_00000UL;
|
||||||
private const int DefaultInheritSched = 0;
|
private const int DefaultInheritSched = 4;
|
||||||
private const int DefaultSchedPolicy = 0;
|
private const int DefaultSchedPolicy = 1;
|
||||||
private const int DefaultSchedPriority = 0;
|
private const int DefaultSchedPriority = DefaultThreadPriority;
|
||||||
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
|
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
|
||||||
private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_0000_0000;
|
private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_0000_0000;
|
||||||
|
|
||||||
@@ -26,12 +27,54 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
|
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
|
||||||
private static readonly Dictionary<ulong, PthreadAttrState> _attrStates = new();
|
private static readonly Dictionary<ulong, PthreadAttrState> _attrStates = new();
|
||||||
private static readonly Dictionary<ulong, PthreadRwlockState> _rwlockStates = new();
|
private static readonly Dictionary<ulong, PthreadRwlockState> _rwlockStates = new();
|
||||||
private static readonly Dictionary<int, TlsKeyState> _tlsKeys = new();
|
private static readonly ConcurrentDictionary<int, TlsKeyState> _tlsKeys = new();
|
||||||
private static int _nextTlsKey = 1;
|
private static int _nextTlsKey = 1;
|
||||||
private static long _nextSyntheticRwlockHandleId = 1;
|
private static long _nextSyntheticRwlockHandleId = 1;
|
||||||
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
||||||
|
|
||||||
private static readonly Dictionary<ulong, Dictionary<int, ulong>> _threadLocalSpecific = new();
|
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
||||||
|
|
||||||
|
internal static void GetThreadStartScheduling(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong attrAddress,
|
||||||
|
out int priority,
|
||||||
|
out ulong affinityMask)
|
||||||
|
{
|
||||||
|
if (attrAddress == 0)
|
||||||
|
{
|
||||||
|
priority = DefaultThreadPriority;
|
||||||
|
affinityMask = DefaultThreadAffinityMask;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var attributes = GetOrCreateAttrStateLocked(resolvedAddress);
|
||||||
|
priority = attributes.SchedPriority;
|
||||||
|
affinityMask = attributes.AffinityMask;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void RegisterThreadStart(
|
||||||
|
ulong thread,
|
||||||
|
string name,
|
||||||
|
int priority,
|
||||||
|
ulong affinityMask)
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var state = GetOrCreateThreadStateLocked(thread);
|
||||||
|
state.Name = name;
|
||||||
|
state.Priority = priority;
|
||||||
|
state.AffinityMask = affinityMask;
|
||||||
|
state.Attributes = state.Attributes with
|
||||||
|
{
|
||||||
|
SchedPriority = priority,
|
||||||
|
AffinityMask = affinityMask,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class ThreadState
|
private sealed class ThreadState
|
||||||
{
|
{
|
||||||
@@ -185,6 +228,35 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rcrVFJsQWRY",
|
||||||
|
ExportName = "scePthreadGetaffinity",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadGetaffinity(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var thread = ctx[CpuRegister.Rdi];
|
||||||
|
var outMaskAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (thread == 0 || outMaskAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong affinityMask;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
affinityMask = GetOrCreateThreadStateLocked(thread).AffinityMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(outMaskAddress, affinityMask))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "1tKyG7RlMJo",
|
Nid = "1tKyG7RlMJo",
|
||||||
ExportName = "scePthreadGetprio",
|
ExportName = "scePthreadGetprio",
|
||||||
@@ -489,6 +561,37 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-quPa4SEJUw",
|
||||||
|
ExportName = "scePthreadAttrGetstack",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadAttrGetstack(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var attrAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var outStackAddressPointer = ctx[CpuRegister.Rsi];
|
||||||
|
var outStackSizeAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (attrAddress == 0 || outStackAddressPointer == 0 || outStackSizeAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
PthreadAttrState state;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(outStackAddressPointer, state.StackAddress) ||
|
||||||
|
!ctx.TryWriteUInt64(outStackSizeAddress, state.StackSize))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "-fA+7ZlGDQs",
|
Nid = "-fA+7ZlGDQs",
|
||||||
ExportName = "scePthreadAttrGetstacksize",
|
ExportName = "scePthreadAttrGetstacksize",
|
||||||
@@ -733,7 +836,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
_rwlockStates[syntheticHandle] = rwlock;
|
_rwlockStates[syntheticHandle] = rwlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -787,7 +890,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt64(rwlockAddress, 0);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, 0);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -899,15 +1002,13 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
int key;
|
int key;
|
||||||
lock (_stateGate)
|
while (true)
|
||||||
{
|
{
|
||||||
while (_tlsKeys.ContainsKey(_nextTlsKey))
|
key = Interlocked.Increment(ref _nextTlsKey) - 1;
|
||||||
|
if (_tlsKeys.TryAdd(key, new TlsKeyState(destructor)))
|
||||||
{
|
{
|
||||||
_nextTlsKey++;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
key = _nextTlsKey++;
|
|
||||||
_tlsKeys[key] = new TlsKeyState(destructor);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteInt32(ctx, outKeyAddress, key))
|
if (!TryWriteInt32(ctx, outKeyAddress, key))
|
||||||
@@ -934,17 +1035,14 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
public static int PosixPthreadKeyDelete(CpuContext ctx)
|
public static int PosixPthreadKeyDelete(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
lock (_stateGate)
|
if (!_tlsKeys.TryRemove(key, out _))
|
||||||
{
|
{
|
||||||
if (!_tlsKeys.Remove(key))
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
{
|
}
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
foreach (var entry in _threadLocalSpecific)
|
foreach (var values in _threadLocalSpecific.Values)
|
||||||
{
|
{
|
||||||
entry.Value.Remove(key);
|
values.TryRemove(key, out _);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -968,22 +1066,15 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var value = ctx[CpuRegister.Rsi];
|
var value = ctx[CpuRegister.Rsi];
|
||||||
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
lock (_stateGate)
|
if (!_tlsKeys.ContainsKey(key))
|
||||||
{
|
{
|
||||||
if (!_tlsKeys.TryGetValue(key, out _))
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values))
|
|
||||||
{
|
|
||||||
values = new Dictionary<int, ulong>();
|
|
||||||
_threadLocalSpecific[currentThreadHandle] = values;
|
|
||||||
}
|
|
||||||
|
|
||||||
values[key] = value;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var values = _threadLocalSpecific.GetOrAdd(
|
||||||
|
currentThreadHandle,
|
||||||
|
static _ => new ConcurrentDictionary<int, ulong>());
|
||||||
|
values[key] = value;
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -1005,19 +1096,16 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
ulong value = 0;
|
ulong value = 0;
|
||||||
lock (_stateGate)
|
if (!_tlsKeys.ContainsKey(key))
|
||||||
{
|
{
|
||||||
if (!_tlsKeys.TryGetValue(key, out _))
|
ctx[CpuRegister.Rax] = 0;
|
||||||
{
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
ctx[CpuRegister.Rax] = 0;
|
}
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) &&
|
if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) &&
|
||||||
values.TryGetValue(key, out var storedValue))
|
values.TryGetValue(key, out var storedValue))
|
||||||
{
|
{
|
||||||
value = storedValue;
|
value = storedValue;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = value;
|
ctx[CpuRegister.Rax] = value;
|
||||||
@@ -1103,7 +1191,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.TryReadUInt64(rwlockAddress, out var pointedHandle) && pointedHandle != 0)
|
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, rwlockAddress, out var pointedHandle) && pointedHandle != 0)
|
||||||
{
|
{
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
@@ -1135,7 +1223,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt64(rwlockAddress, out var pointedHandle))
|
if (!KernelMemoryCompatExports.TryReadUInt64Compat(ctx, rwlockAddress, out var pointedHandle))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1170,7 +1258,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
_rwlockStates[syntheticHandle] = createdRwlock;
|
_rwlockStates[syntheticHandle] = createdRwlock;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle);
|
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle);
|
||||||
resolvedAddress = syntheticHandle;
|
resolvedAddress = syntheticHandle;
|
||||||
rwlock = createdRwlock;
|
rwlock = createdRwlock;
|
||||||
return true;
|
return true;
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
@@ -11,8 +12,7 @@ internal static class KernelPthreadState
|
|||||||
{
|
{
|
||||||
private const int ThreadObjectSize = 0x1000;
|
private const int ThreadObjectSize = 0x1000;
|
||||||
|
|
||||||
private static readonly object Gate = new();
|
private static readonly ConcurrentDictionary<ulong, ThreadIdentity> Threads = new();
|
||||||
private static readonly Dictionary<ulong, ThreadIdentity> Threads = new();
|
|
||||||
private static readonly byte[] ZeroThreadObject = new byte[ThreadObjectSize];
|
private static readonly byte[] ZeroThreadObject = new byte[ThreadObjectSize];
|
||||||
private static long _nextUniqueThreadId = 1;
|
private static long _nextUniqueThreadId = 1;
|
||||||
|
|
||||||
@@ -56,10 +56,7 @@ internal static class KernelPthreadState
|
|||||||
|
|
||||||
internal static bool TryGetThreadIdentity(ulong threadHandle, out ThreadIdentity identity)
|
internal static bool TryGetThreadIdentity(ulong threadHandle, out ThreadIdentity identity)
|
||||||
{
|
{
|
||||||
lock (Gate)
|
return Threads.TryGetValue(threadHandle, out identity);
|
||||||
{
|
|
||||||
return Threads.TryGetValue(threadHandle, out identity);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void EnsureCurrentThreadRegistered()
|
private static void EnsureCurrentThreadRegistered()
|
||||||
@@ -81,10 +78,7 @@ internal static class KernelPthreadState
|
|||||||
Marshal.Copy(ZeroThreadObject, 0, pointer, ThreadObjectSize);
|
Marshal.Copy(ZeroThreadObject, 0, pointer, ThreadObjectSize);
|
||||||
|
|
||||||
var handle = unchecked((ulong)pointer.ToInt64());
|
var handle = unchecked((ulong)pointer.ToInt64());
|
||||||
lock (Gate)
|
Threads[handle] = new ThreadIdentity(uniqueId, string.IsNullOrWhiteSpace(name) ? $"Thread-{uniqueId:X}" : name);
|
||||||
{
|
|
||||||
Threads[handle] = new ThreadIdentity(uniqueId, string.IsNullOrWhiteSpace(name) ? $"Thread-{uniqueId:X}" : name);
|
|
||||||
}
|
|
||||||
|
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Fiber;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
@@ -25,12 +26,15 @@ public static class KernelRuntimeCompatExports
|
|||||||
private const ulong TlsNewReplaceOffset = 0x300;
|
private const ulong TlsNewReplaceOffset = 0x300;
|
||||||
private const int MallocReplaceSize = 0x70;
|
private const int MallocReplaceSize = 0x70;
|
||||||
private const int NewReplaceSize = 0x68;
|
private const int NewReplaceSize = 0x68;
|
||||||
|
private const int OrbisTimesecSize = sizeof(long) + sizeof(uint) + sizeof(uint);
|
||||||
private const ulong ModuleInfoHandleOffset = 0x108;
|
private const ulong ModuleInfoHandleOffset = 0x108;
|
||||||
private const ulong ModuleInfoNameOffset = 0x10;
|
private const ulong ModuleInfoNameOffset = 0x10;
|
||||||
private const int ModuleInfoNameMaxBytes = 64;
|
private const int ModuleInfoNameMaxBytes = 64;
|
||||||
private const ulong DefaultKernelTscFrequency = 10_000_000UL;
|
private const ulong DefaultKernelTscFrequency = 10_000_000UL;
|
||||||
private const ulong PrtAreaStartAddress = 0x0000001000000000UL;
|
private const ulong PrtAreaStartAddress = 0x0000001000000000UL;
|
||||||
private const ulong PrtAreaSize = 0x000000EC00000000UL;
|
private const ulong PrtAreaSize = 0x000000EC00000000UL;
|
||||||
|
private const int MapFlagFixed = 0x10;
|
||||||
|
private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
|
||||||
private const int AioInitParamSize = 0x3C;
|
private const int AioInitParamSize = 0x3C;
|
||||||
private const uint MemCommit = 0x1000;
|
private const uint MemCommit = 0x1000;
|
||||||
private const uint MemReserve = 0x2000;
|
private const uint MemReserve = 0x2000;
|
||||||
@@ -52,6 +56,14 @@ public static class KernelRuntimeCompatExports
|
|||||||
private static readonly object _prtApertureGate = new();
|
private static readonly object _prtApertureGate = new();
|
||||||
private static readonly (ulong Base, ulong Size)[] _prtApertures = new (ulong Base, ulong Size)[3];
|
private static readonly (ulong Base, ulong Size)[] _prtApertures = new (ulong Base, ulong Size)[3];
|
||||||
private static int _stackChkFailCount;
|
private static int _stackChkFailCount;
|
||||||
|
private static long _usleepTraceCount;
|
||||||
|
private static readonly bool _traceUsleep =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal);
|
||||||
|
private static readonly bool _traceGuestThreads =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static int _shortUsleepCount;
|
||||||
|
|
||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
private delegate ulong RdtscDelegate();
|
private delegate ulong RdtscDelegate();
|
||||||
@@ -64,33 +76,104 @@ public static class KernelRuntimeCompatExports
|
|||||||
public static int KernelUsleep(CpuContext ctx)
|
public static int KernelUsleep(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var micros = ctx[CpuRegister.Rdi];
|
var micros = ctx[CpuRegister.Rdi];
|
||||||
|
TraceUsleepSpin(ctx, micros);
|
||||||
if (micros == 0)
|
if (micros == 0)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
var sleepMilliseconds = (int)Math.Min(micros / 1000UL, int.MaxValue);
|
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelUsleep");
|
||||||
if (sleepMilliseconds > 0)
|
|
||||||
{
|
|
||||||
Thread.Sleep(sleepMilliseconds);
|
|
||||||
}
|
|
||||||
|
|
||||||
var remainingMicros = micros % 1000UL;
|
if (micros < 1000)
|
||||||
if (remainingMicros > 0)
|
|
||||||
{
|
{
|
||||||
var targetTicks = (long)((double)remainingMicros * Stopwatch.Frequency / 1_000_000.0);
|
// Guest worker pools use usleep(1) as a polling backoff. Periodically
|
||||||
var spin = Stopwatch.StartNew();
|
// relinquish a full host time slice so spin workers cannot starve producers.
|
||||||
while (spin.ElapsedTicks < targetTicks)
|
if ((++_shortUsleepCount & 31) == 0)
|
||||||
{
|
{
|
||||||
Thread.SpinWait(16);
|
Thread.Sleep(1);
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Thread.Yield();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_shortUsleepCount = 0;
|
||||||
|
var sleepMilliseconds = (int)Math.Min((micros + 999UL) / 1000UL, int.MaxValue);
|
||||||
|
Thread.Sleep(sleepMilliseconds);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void TraceUsleepSpin(CpuContext ctx, ulong micros)
|
||||||
|
{
|
||||||
|
if (!_traceUsleep)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = Interlocked.Increment(ref _usleepTraceCount);
|
||||||
|
if (count > 32 && count % 10000 != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var rbx = ctx[CpuRegister.Rbx];
|
||||||
|
var r12 = ctx[CpuRegister.R12];
|
||||||
|
var r13 = ctx[CpuRegister.R13];
|
||||||
|
var lockAddress = rbx == 0 ? 0 : rbx + 0xF78;
|
||||||
|
var lockText = "unreadable";
|
||||||
|
if (lockAddress != 0 && ctx.TryReadUInt64(lockAddress, out var lockValue))
|
||||||
|
{
|
||||||
|
lockText = $"0x{lockValue:X16}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var schedulerText = "unreadable";
|
||||||
|
if (r12 != 0 && ctx.TryReadUInt64(r12 + 8, out var schedulerAddress))
|
||||||
|
{
|
||||||
|
schedulerText = $"0x{schedulerAddress:X16}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var waitValueText = "unreadable";
|
||||||
|
if (r13 != 0 && ctx.TryReadUInt64(r13, out var waitValue))
|
||||||
|
{
|
||||||
|
waitValueText = $"0x{waitValue:X16}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var callerReturnText = "unreadable";
|
||||||
|
var rbp = ctx[CpuRegister.Rbp];
|
||||||
|
if (rbp != 0 && ctx.TryReadUInt64(rbp + 8, out var callerReturn))
|
||||||
|
{
|
||||||
|
callerReturnText = $"0x{callerReturn:X16}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var returnRip = GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame)
|
||||||
|
? frame.ReturnRip
|
||||||
|
: 0UL;
|
||||||
|
var thread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
|
var fiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] usleep#{count}: usec={micros} ret=0x{returnRip:X16} caller={callerReturnText} thread=0x{thread:X16} fiber=0x{fiber:X16} rbx=0x{rbx:X16} lock@+F78=0x{lockAddress:X16}:{lockText} r12=0x{r12:X16} scheduler@+8={schedulerText} r13=0x{r13:X16}:{waitValueText} r14=0x{ctx[CpuRegister.R14]:X16} r15=0x{ctx[CpuRegister.R15]:X16}");
|
||||||
|
|
||||||
|
if (count % 100000 == 0 &&
|
||||||
|
_traceGuestThreads &&
|
||||||
|
GuestThreadExecution.Scheduler is { } scheduler)
|
||||||
|
{
|
||||||
|
foreach (var snapshot in scheduler.SnapshotThreads())
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] guest_thread.snapshot handle=0x{snapshot.ThreadHandle:X16} name='{snapshot.Name}' " +
|
||||||
|
$"state={snapshot.State} imports={snapshot.ImportCount} nid={snapshot.LastImportNid ?? "none"} " +
|
||||||
|
$"ret=0x{snapshot.LastReturnRip:X16} block={snapshot.BlockReason ?? "none"}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "QBi7HCK03hw",
|
Nid = "QBi7HCK03hw",
|
||||||
ExportName = "sceKernelClockGettime",
|
ExportName = "sceKernelClockGettime",
|
||||||
@@ -156,6 +239,37 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "n88vx3C5nW8",
|
||||||
|
ExportName = "gettimeofday",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixGettimeofday(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var timezoneAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
var seconds = now.ToUnixTimeSeconds();
|
||||||
|
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
|
||||||
|
|
||||||
|
if (timeAddress != 0 &&
|
||||||
|
(!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
|
||||||
|
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds))))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timezoneAddress != 0 &&
|
||||||
|
(!TryWriteInt32(ctx, timezoneAddress, 0) ||
|
||||||
|
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), 0)))
|
||||||
|
{
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "-2IRUCO--PM",
|
Nid = "-2IRUCO--PM",
|
||||||
ExportName = "sceKernelReadTsc",
|
ExportName = "sceKernelReadTsc",
|
||||||
@@ -497,19 +611,16 @@ public static class KernelRuntimeCompatExports
|
|||||||
public static int ErrorAddress(CpuContext ctx)
|
public static int ErrorAddress(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
||||||
if (address != 0)
|
|
||||||
{
|
|
||||||
Span<byte> zero = stackalloc byte[sizeof(int)];
|
|
||||||
if (!ctx.Memory.TryWrite(address, zero))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = address;
|
ctx[CpuRegister.Rax] = address;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool TrySetErrno(CpuContext ctx, int value)
|
||||||
|
{
|
||||||
|
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
||||||
|
return address != 0 && TryWriteInt32(ctx, address, value);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "bnZxYgAFeA0",
|
Nid = "bnZxYgAFeA0",
|
||||||
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
|
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
|
||||||
@@ -552,6 +663,17 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "jh+8XiK4LeE",
|
||||||
|
ExportName = "sceKernelIsAddressSanitizerEnabled",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelIsAddressSanitizerEnabled(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ca7v6Cxulzs",
|
Nid = "ca7v6Cxulzs",
|
||||||
ExportName = "sceKernelSetGPO",
|
ExportName = "sceKernelSetGPO",
|
||||||
@@ -590,7 +712,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
{
|
{
|
||||||
var inOutAddressPointer = ctx[CpuRegister.Rdi];
|
var inOutAddressPointer = ctx[CpuRegister.Rdi];
|
||||||
var length = ctx[CpuRegister.Rsi];
|
var length = ctx[CpuRegister.Rsi];
|
||||||
var _flags = ctx[CpuRegister.Rdx];
|
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
var alignment = ctx[CpuRegister.Rcx];
|
var alignment = ctx[CpuRegister.Rcx];
|
||||||
if (inOutAddressPointer == 0 || length == 0)
|
if (inOutAddressPointer == 0 || length == 0)
|
||||||
{
|
{
|
||||||
@@ -602,7 +724,8 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var effectiveAlignment = alignment == 0 ? 0x10000UL : alignment;
|
var effectiveAlignment = alignment == 0 ? DefaultVirtualRangeAlignment : alignment;
|
||||||
|
var fixedMapping = (flags & MapFlagFixed) != 0;
|
||||||
ulong desiredAddress;
|
ulong desiredAddress;
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
@@ -611,13 +734,13 @@ public static class KernelRuntimeCompatExports
|
|||||||
: AlignUp(_nextReservedVirtualBase, effectiveAlignment);
|
: AlignUp(_nextReservedVirtualBase, effectiveAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReserveVirtualRange(ctx, desiredAddress, length, out var mappedAddress))
|
if (!TryReserveVirtualRange(ctx, desiredAddress, length, effectiveAlignment, allowSearch: !fixedMapping, out var mappedAddress))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16}");
|
$"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16} flags=0x{flags:X8} align=0x{effectiveAlignment:X16}");
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
||||||
{
|
{
|
||||||
@@ -901,14 +1024,12 @@ public static class KernelRuntimeCompatExports
|
|||||||
public static int StackCheckFail(CpuContext ctx)
|
public static int StackCheckFail(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var count = Interlocked.Increment(ref _stackChkFailCount);
|
var count = Interlocked.Increment(ref _stackChkFailCount);
|
||||||
if (count <= 8)
|
Console.Error.WriteLine(
|
||||||
{
|
$"[LOADER][ERROR] __stack_chk_fail#{count}: rip=0x{ctx.Rip:X16} rdi=0x{ctx[CpuRegister.Rdi]:X16}");
|
||||||
Console.Error.WriteLine(
|
var result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP;
|
||||||
$"[LOADER][WARNING] __stack_chk_fail recovery#{count}: rip=0x{ctx.Rip:X16} rdi=0x{ctx[CpuRegister.Rdi]:X16}");
|
GuestThreadExecution.RequestCurrentEntryExit("__stack_chk_fail", result);
|
||||||
}
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -1024,6 +1145,111 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-o5uEDpN+oY",
|
||||||
|
ExportName = "sceKernelConvertUtcToLocaltime",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelConvertUtcToLocaltime(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var utcSeconds = unchecked((long)ctx[CpuRegister.Rdi]);
|
||||||
|
var localTimeAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var timesecAddress = ctx[CpuRegister.Rdx];
|
||||||
|
var dstSecondsAddress = ctx[CpuRegister.Rcx];
|
||||||
|
|
||||||
|
if (localTimeAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var utc = DateTimeOffset.FromUnixTimeSeconds(utcSeconds);
|
||||||
|
var local = TimeZoneInfo.ConvertTime(utc, TimeZoneInfo.Local);
|
||||||
|
var offset = local.Offset;
|
||||||
|
var localSeconds = utcSeconds + (long)offset.TotalSeconds;
|
||||||
|
var dstSeconds = TimeZoneInfo.Local.IsDaylightSavingTime(local.DateTime)
|
||||||
|
? (uint)Math.Max(0, TimeZoneInfo.Local.GetAdjustmentRules()
|
||||||
|
.Where(rule => rule.DateStart <= local.Date && rule.DateEnd >= local.Date)
|
||||||
|
.Select(rule => rule.DaylightDelta.TotalSeconds)
|
||||||
|
.DefaultIfEmpty(0)
|
||||||
|
.Max())
|
||||||
|
: 0u;
|
||||||
|
var westSeconds = unchecked((uint)(int)offset.TotalSeconds);
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(localTimeAddress, unchecked((ulong)localSeconds)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (timesecAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> timesec = stackalloc byte[OrbisTimesecSize];
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(timesec, utcSeconds);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(timesec.Slice(sizeof(long), sizeof(uint)), westSeconds);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(timesec.Slice(sizeof(long) + sizeof(uint), sizeof(uint)), dstSeconds);
|
||||||
|
if (!ctx.Memory.TryWrite(timesecAddress, timesec))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dstSecondsAddress != 0 && !ctx.TryWriteUInt64(dstSecondsAddress, dstSeconds))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0NTHN1NKONI",
|
||||||
|
ExportName = "sceKernelConvertLocaltimeToUtc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelConvertLocaltimeToUtc(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var localSeconds = unchecked((long)ctx[CpuRegister.Rdi]);
|
||||||
|
var utcTimeAddress = ctx[CpuRegister.Rdx];
|
||||||
|
var timezoneAddress = ctx[CpuRegister.Rcx];
|
||||||
|
var dstSecondsAddress = ctx[CpuRegister.R8];
|
||||||
|
|
||||||
|
if (timezoneAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var localDate = DateTimeOffset.FromUnixTimeSeconds(localSeconds).DateTime;
|
||||||
|
var offset = TimeZoneInfo.Local.GetUtcOffset(localDate);
|
||||||
|
var utcSeconds = localSeconds - (long)offset.TotalSeconds;
|
||||||
|
var dstSeconds = TimeZoneInfo.Local.IsDaylightSavingTime(localDate)
|
||||||
|
? (int)Math.Max(0, TimeZoneInfo.Local.GetAdjustmentRules()
|
||||||
|
.Where(rule => rule.DateStart <= localDate.Date && rule.DateEnd >= localDate.Date)
|
||||||
|
.Select(rule => rule.DaylightDelta.TotalSeconds)
|
||||||
|
.DefaultIfEmpty(0)
|
||||||
|
.Max())
|
||||||
|
: 0;
|
||||||
|
var minutesWest = unchecked((int)-offset.TotalMinutes);
|
||||||
|
|
||||||
|
if (!TryWriteInt32(ctx, timezoneAddress, minutesWest) ||
|
||||||
|
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), dstSeconds / 60))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (utcTimeAddress != 0 && !ctx.TryWriteUInt64(utcTimeAddress, unchecked((ulong)utcSeconds)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dstSecondsAddress != 0 && !TryWriteInt32(ctx, dstSecondsAddress, dstSeconds))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "vYU8P9Td2Zo",
|
Nid = "vYU8P9Td2Zo",
|
||||||
ExportName = "sceKernelAioInitializeImpl",
|
ExportName = "sceKernelAioInitializeImpl",
|
||||||
@@ -1306,30 +1532,40 @@ public static class KernelRuntimeCompatExports
|
|||||||
ulong.TryParse(overrideHzText, out var overrideHz) &&
|
ulong.TryParse(overrideHzText, out var overrideHz) &&
|
||||||
overrideHz >= minSane)
|
overrideHz >= minSane)
|
||||||
{
|
{
|
||||||
|
TraceKernelTscFrequency("env", overrideHz);
|
||||||
return overrideHz;
|
return overrideHz;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
|
|
||||||
{
|
|
||||||
return cpuidHz;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (TryCalibrateHostTscFrequency(out ulong calibratedHz) && calibratedHz >= minSane)
|
if (TryCalibrateHostTscFrequency(out ulong calibratedHz) && calibratedHz >= minSane)
|
||||||
{
|
{
|
||||||
|
TraceKernelTscFrequency("calibrated-rdtsc", calibratedHz);
|
||||||
return calibratedHz;
|
return calibratedHz;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
|
||||||
|
{
|
||||||
|
TraceKernelTscFrequency("cpuid", cpuidHz);
|
||||||
|
return cpuidHz;
|
||||||
|
}
|
||||||
|
|
||||||
var hostQpc = Stopwatch.Frequency > 0
|
var hostQpc = Stopwatch.Frequency > 0
|
||||||
? unchecked((ulong)Stopwatch.Frequency)
|
? unchecked((ulong)Stopwatch.Frequency)
|
||||||
: DefaultKernelTscFrequency;
|
: DefaultKernelTscFrequency;
|
||||||
if (hostQpc >= minSane)
|
if (hostQpc >= minSane)
|
||||||
{
|
{
|
||||||
|
TraceKernelTscFrequency("qpc", hostQpc);
|
||||||
return hostQpc;
|
return hostQpc;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
TraceKernelTscFrequency("default", DefaultKernelTscFrequency);
|
||||||
return DefaultKernelTscFrequency;
|
return DefaultKernelTscFrequency;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void TraceKernelTscFrequency(string source, ulong frequencyHz)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Kernel TSC frequency: {frequencyHz} Hz ({source})");
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryResolveCpuidTscFrequency(out ulong frequencyHz)
|
private static bool TryResolveCpuidTscFrequency(out ulong frequencyHz)
|
||||||
{
|
{
|
||||||
frequencyHz = 0;
|
frequencyHz = 0;
|
||||||
@@ -1488,7 +1724,13 @@ public static class KernelRuntimeCompatExports
|
|||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||||
|
|
||||||
private static bool TryReserveVirtualRange(CpuContext ctx, ulong desiredAddress, ulong length, out ulong mappedAddress)
|
private static bool TryReserveVirtualRange(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong desiredAddress,
|
||||||
|
ulong length,
|
||||||
|
ulong alignment,
|
||||||
|
bool allowSearch,
|
||||||
|
out ulong mappedAddress)
|
||||||
{
|
{
|
||||||
mappedAddress = 0;
|
mappedAddress = 0;
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
@@ -1500,40 +1742,51 @@ public static class KernelRuntimeCompatExports
|
|||||||
{
|
{
|
||||||
object memoryObject = ctx.Memory;
|
object memoryObject = ctx.Memory;
|
||||||
MethodInfo? allocateAt = null;
|
MethodInfo? allocateAt = null;
|
||||||
|
MethodInfo? allocateAtOrAbove = null;
|
||||||
var allocateAtHasAllowAlternativeArg = false;
|
var allocateAtHasAllowAlternativeArg = false;
|
||||||
for (var depth = 0; depth < 4; depth++)
|
for (var depth = 0; depth < 4; depth++)
|
||||||
{
|
{
|
||||||
foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
||||||
{
|
{
|
||||||
if (!string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
var parameters = candidate.GetParameters();
|
var parameters = candidate.GetParameters();
|
||||||
if (parameters.Length == 3 &&
|
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
parameters.Length == 5 &&
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = false;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (parameters.Length == 4 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
parameters[0].ParameterType == typeof(ulong) &&
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
parameters[1].ParameterType == typeof(ulong) &&
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
parameters[2].ParameterType == typeof(bool) &&
|
||||||
parameters[3].ParameterType == typeof(bool))
|
parameters[3].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
|
||||||
|
{
|
||||||
|
allocateAtOrAbove = candidate;
|
||||||
|
}
|
||||||
|
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (parameters.Length == 3 &&
|
||||||
|
parameters[0].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[1].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[2].ParameterType == typeof(bool))
|
||||||
|
{
|
||||||
|
allocateAt = candidate;
|
||||||
|
allocateAtHasAllowAlternativeArg = false;
|
||||||
|
}
|
||||||
|
else if (parameters.Length == 4 &&
|
||||||
|
parameters[0].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[1].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[2].ParameterType == typeof(bool) &&
|
||||||
|
parameters[3].ParameterType == typeof(bool))
|
||||||
|
{
|
||||||
|
allocateAt = candidate;
|
||||||
|
allocateAtHasAllowAlternativeArg = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allocateAtOrAbove is not null && allocateAt is not null)
|
||||||
{
|
{
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = true;
|
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (allocateAt is not null)
|
if (allocateAtOrAbove is not null || allocateAt is not null)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1553,6 +1806,18 @@ public static class KernelRuntimeCompatExports
|
|||||||
memoryObject = innerValue;
|
memoryObject = innerValue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (allowSearch && allocateAtOrAbove is not null)
|
||||||
|
{
|
||||||
|
var searchArgs = new object[] { desiredAddress, length, false, alignment, 0UL };
|
||||||
|
var searchResult = allocateAtOrAbove.Invoke(memoryObject, searchArgs);
|
||||||
|
if (searchResult is bool trueValue && trueValue &&
|
||||||
|
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
||||||
|
{
|
||||||
|
mappedAddress = searchedAddress;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (allocateAt is null)
|
if (allocateAt is null)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
||||||
@@ -1560,7 +1825,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var invokeArgs = allocateAtHasAllowAlternativeArg
|
var invokeArgs = allocateAtHasAllowAlternativeArg
|
||||||
? new object[] { desiredAddress, length, false, true }
|
? new object[] { desiredAddress, length, false, allowSearch }
|
||||||
: new object[] { desiredAddress, length, false };
|
: new object[] { desiredAddress, length, false };
|
||||||
var result = allocateAt.Invoke(memoryObject, invokeArgs);
|
var result = allocateAt.Invoke(memoryObject, invokeArgs);
|
||||||
if (result is not ulong allocated || allocated == 0)
|
if (result is not ulong allocated || allocated == 0)
|
||||||
|
|||||||
@@ -120,7 +120,7 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitSema"))
|
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
|
||||||
{
|
{
|
||||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
|
|||||||
@@ -0,0 +1,79 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Network;
|
||||||
|
|
||||||
|
public static class Http2Exports
|
||||||
|
{
|
||||||
|
private const int Http2ErrorInvalidId = unchecked((int)0x80436004);
|
||||||
|
private const int Http2ErrorInvalidArgument = unchecked((int)0x80436016);
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<int, Http2Context> _contexts = new();
|
||||||
|
private static int _nextContextId;
|
||||||
|
|
||||||
|
private sealed record Http2Context(int NetId, int SslId, ulong PoolSize, int MaxRequests);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3JCe3lCbQ8A",
|
||||||
|
ExportName = "sceHttp2Init",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2Init(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var netId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var sslId = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var poolSize = ctx[CpuRegister.Rdx];
|
||||||
|
var maxRequests = unchecked((int)ctx[CpuRegister.Rcx]);
|
||||||
|
|
||||||
|
if (poolSize == 0 || maxRequests <= 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, Http2ErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = Interlocked.Increment(ref _nextContextId);
|
||||||
|
_contexts[id] = new Http2Context(netId, sslId, poolSize, maxRequests);
|
||||||
|
|
||||||
|
TraceHttp2("init", id, unchecked((ulong)netId), unchecked((ulong)sslId), poolSize, unchecked((ulong)maxRequests));
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)id);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "YiBUtz-pGkc",
|
||||||
|
ExportName = "sceHttp2Term",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp2")]
|
||||||
|
public static int Http2Term(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_contexts.TryRemove(id, out _))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, Http2ErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceHttp2("term", id, 0, 0, 0, 0);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceHttp2(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_HTTP2"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] http2.{operation} id={id} arg0=0x{arg0:X16} arg1=0x{arg1:X16} arg2=0x{arg2:X16} arg3=0x{arg3:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,122 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Network;
|
||||||
|
|
||||||
|
public static class HttpExports
|
||||||
|
{
|
||||||
|
private const int HttpErrorInvalidId = unchecked((int)0x80431100);
|
||||||
|
private const int HttpErrorInvalidValue = unchecked((int)0x804311FE);
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<int, HttpContext> Contexts = new();
|
||||||
|
private static readonly ConcurrentDictionary<int, HttpTemplate> Templates = new();
|
||||||
|
private static int _nextContextId;
|
||||||
|
private static int _nextTemplateId = 0x1000;
|
||||||
|
|
||||||
|
private sealed record HttpContext(int NetMemoryId, int SslContextId, ulong PoolSize);
|
||||||
|
|
||||||
|
private sealed record HttpTemplate(int ContextId, ulong UserAgentAddress, int HttpVersion, bool AutoProxyConfig);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "A9cVMUtEp4Y",
|
||||||
|
ExportName = "sceHttpInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp")]
|
||||||
|
public static int HttpInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var netMemoryId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var sslContextId = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var poolSize = ctx[CpuRegister.Rdx];
|
||||||
|
if (poolSize == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, HttpErrorInvalidValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = Interlocked.Increment(ref _nextContextId);
|
||||||
|
Contexts[id] = new HttpContext(netMemoryId, sslContextId, poolSize);
|
||||||
|
TraceHttp("init", id, unchecked((ulong)netMemoryId), unchecked((ulong)sslContextId), poolSize, 0);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)id);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0gYjPTR-6cY",
|
||||||
|
ExportName = "sceHttpCreateTemplate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp")]
|
||||||
|
public static int HttpCreateTemplate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!Contexts.ContainsKey(contextId))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, HttpErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
var userAgentAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var httpVersion = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
var autoProxyConfig = ctx[CpuRegister.Rcx] != 0;
|
||||||
|
var id = Interlocked.Increment(ref _nextTemplateId);
|
||||||
|
Templates[id] = new HttpTemplate(contextId, userAgentAddress, httpVersion, autoProxyConfig);
|
||||||
|
TraceHttp("create_template", id, unchecked((ulong)contextId), userAgentAddress, unchecked((ulong)httpVersion), autoProxyConfig ? 1UL : 0UL);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)id);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4I8vEpuEhZ8",
|
||||||
|
ExportName = "sceHttpDeleteTemplate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp")]
|
||||||
|
public static int HttpDeleteTemplate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
return Templates.TryRemove(templateId, out _)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, HttpErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ik-KpLTlf7Q",
|
||||||
|
ExportName = "sceHttpTerm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceHttp")]
|
||||||
|
public static int HttpTerm(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!Contexts.TryRemove(contextId, out _))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, HttpErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var pair in Templates)
|
||||||
|
{
|
||||||
|
if (pair.Value.ContextId == contextId)
|
||||||
|
{
|
||||||
|
Templates.TryRemove(pair.Key, out _);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_HTTP"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] http.{operation} id={id} arg0=0x{arg0:X16} arg1=0x{arg1:X16} arg2=0x{arg2:X16} arg3=0x{arg3:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,229 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Network;
|
||||||
|
|
||||||
|
public static class NetCtlExports
|
||||||
|
{
|
||||||
|
private const int MaxCallbacks = 8;
|
||||||
|
private const int NatInfoSize = 16;
|
||||||
|
private const int NetCtlErrorNoSpace = unchecked((int)0x80412103);
|
||||||
|
private const int NetCtlErrorInvalidAddress = unchecked((int)0x80412107);
|
||||||
|
private const int NetCtlErrorNotConnected = unchecked((int)0x80412108);
|
||||||
|
private const int NetCtlInfoDevice = 1;
|
||||||
|
private const int NetCtlInfoEtherAddress = 2;
|
||||||
|
private const int NetCtlInfoMtu = 3;
|
||||||
|
private const int NetCtlInfoLink = 4;
|
||||||
|
private const int NetCtlInfoIpConfig = 11;
|
||||||
|
private const int NetCtlInfoDhcpHostname = 12;
|
||||||
|
private const int NetCtlInfoPppoeAuthName = 13;
|
||||||
|
private const int NetCtlInfoIpAddress = 14;
|
||||||
|
private const int NetCtlInfoNetmask = 15;
|
||||||
|
private const int NetCtlInfoDefaultRoute = 16;
|
||||||
|
private const int NetCtlInfoPrimaryDns = 17;
|
||||||
|
private const int NetCtlInfoSecondaryDns = 18;
|
||||||
|
private const int NetCtlInfoHttpProxyConfig = 19;
|
||||||
|
private const int NetCtlInfoHttpProxyServer = 20;
|
||||||
|
private const int NetCtlInfoHttpProxyPort = 21;
|
||||||
|
private const int NetCtlDeviceWired = 0;
|
||||||
|
private const int NetCtlLinkDisconnected = 0;
|
||||||
|
private const int NetCtlIpConfigStatic = 0;
|
||||||
|
private static readonly object CallbackGate = new();
|
||||||
|
private static readonly CallbackRegistration[] Callbacks = new CallbackRegistration[MaxCallbacks];
|
||||||
|
|
||||||
|
private readonly record struct CallbackRegistration(ulong Function, ulong Argument);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gky0+oaNM4k",
|
||||||
|
ExportName = "sceNetCtlInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNetCtl")]
|
||||||
|
public static int NetCtlInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JO4yuTuMoKI",
|
||||||
|
ExportName = "sceNetCtlGetNatInfo",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNetCtl")]
|
||||||
|
public static int NetCtlGetNatInfo(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var natInfoAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (natInfoAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> natInfo = stackalloc byte[NatInfoSize];
|
||||||
|
if (!ctx.Memory.TryRead(natInfoAddress, natInfo))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]);
|
||||||
|
if (size != NatInfoSize)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(natInfo[4..], 1);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(natInfo[8..], 3);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001);
|
||||||
|
return ctx.Memory.TryWrite(natInfoAddress, natInfo)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "iQw3iQPhvUQ",
|
||||||
|
ExportName = "sceNetCtlCheckCallback",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNetCtl")]
|
||||||
|
public static int NetCtlCheckCallback(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uBPlr0lbuiI",
|
||||||
|
ExportName = "sceNetCtlGetState",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNetCtl")]
|
||||||
|
public static int NetCtlGetState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var stateAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (stateAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> stateBytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0);
|
||||||
|
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "UJ+Z7Q+4ck0",
|
||||||
|
ExportName = "sceNetCtlRegisterCallback",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNetCtl")]
|
||||||
|
public static int NetCtlRegisterCallback(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var function = ctx[CpuRegister.Rdi];
|
||||||
|
var argument = ctx[CpuRegister.Rsi];
|
||||||
|
var callbackIdAddress = ctx[CpuRegister.Rdx];
|
||||||
|
if (function == 0 || callbackIdAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (CallbackGate)
|
||||||
|
{
|
||||||
|
var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0);
|
||||||
|
if (callbackId < 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NetCtlErrorNoSpace);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> callbackIdBytes = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId));
|
||||||
|
if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Callbacks[callbackId] = new CallbackRegistration(function, argument);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "obuxdTiwkF8",
|
||||||
|
ExportName = "sceNetCtlGetInfo",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNetCtl")]
|
||||||
|
public static int NetCtlGetInfo(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var code = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var infoAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (infoAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
return code switch
|
||||||
|
{
|
||||||
|
NetCtlInfoDevice => WriteUInt32(ctx, infoAddress, NetCtlDeviceWired),
|
||||||
|
NetCtlInfoEtherAddress => WriteZeroBytes(ctx, infoAddress, 6),
|
||||||
|
NetCtlInfoMtu => WriteUInt32(ctx, infoAddress, 1500),
|
||||||
|
NetCtlInfoLink => WriteUInt32(ctx, infoAddress, NetCtlLinkDisconnected),
|
||||||
|
NetCtlInfoIpConfig => WriteUInt32(ctx, infoAddress, NetCtlIpConfigStatic),
|
||||||
|
NetCtlInfoDhcpHostname => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
|
||||||
|
NetCtlInfoPppoeAuthName => WriteAsciiZ(ctx, infoAddress, string.Empty, 128),
|
||||||
|
NetCtlInfoIpAddress => WriteAsciiZ(ctx, infoAddress, "127.0.0.1", 16),
|
||||||
|
NetCtlInfoNetmask => WriteAsciiZ(ctx, infoAddress, "255.0.0.0", 16),
|
||||||
|
NetCtlInfoDefaultRoute => WriteAsciiZ(ctx, infoAddress, "127.0.0.1", 16),
|
||||||
|
NetCtlInfoPrimaryDns => WriteAsciiZ(ctx, infoAddress, "1.1.1.1", 16),
|
||||||
|
NetCtlInfoSecondaryDns => WriteAsciiZ(ctx, infoAddress, "1.1.1.1", 16),
|
||||||
|
NetCtlInfoHttpProxyConfig => WriteUInt32(ctx, infoAddress, 0),
|
||||||
|
NetCtlInfoHttpProxyServer => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
|
||||||
|
NetCtlInfoHttpProxyPort => WriteUInt16(ctx, infoAddress, 0),
|
||||||
|
_ => SetReturn(ctx, NetCtlErrorNotConnected),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int WriteZeroBytes(CpuContext ctx, ulong address, int count)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[count];
|
||||||
|
return ctx.Memory.TryWrite(address, bytes)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int WriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int WriteUInt16(CpuContext ctx, ulong address, ushort value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int WriteAsciiZ(CpuContext ctx, ulong address, string value, int byteCount)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[byteCount];
|
||||||
|
var copyCount = Math.Min(value.Length, byteCount - 1);
|
||||||
|
for (var i = 0; i < copyCount; i++)
|
||||||
|
{
|
||||||
|
bytes[i] = (byte)value[i];
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(address, bytes)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,210 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Network;
|
||||||
|
|
||||||
|
public static class NetExports
|
||||||
|
{
|
||||||
|
private const int NetErrorBadFileDescriptor = unchecked((int)0x80410109);
|
||||||
|
private const int NetErrorInvalidArgument = unchecked((int)0x80410116);
|
||||||
|
private const int MaxNameLength = 256;
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<int, NetPool> _pools = new();
|
||||||
|
private static readonly ConcurrentDictionary<int, ResolverContext> _resolvers = new();
|
||||||
|
private static int _nextPoolId;
|
||||||
|
private static int _nextResolverId = 0x2000;
|
||||||
|
private static bool _initialized;
|
||||||
|
|
||||||
|
private sealed record NetPool(string Name, int Size, int Flags);
|
||||||
|
|
||||||
|
private sealed record ResolverContext(string Name, int PoolId, int Flags, int LastError);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Nlev7Lg8k3A",
|
||||||
|
ExportName = "sceNetInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_initialized = true;
|
||||||
|
TraceNet("init", 0, 0, 0, 0);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cTGkc6-TBlI",
|
||||||
|
ExportName = "sceNetTerm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetTerm(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_initialized = false;
|
||||||
|
_pools.Clear();
|
||||||
|
_resolvers.Clear();
|
||||||
|
TraceNet("term", 0, 0, 0, 0);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "dgJBaeJnGpo",
|
||||||
|
ExportName = "sceNetPoolCreate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetPoolCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var nameAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var size = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
|
||||||
|
if (size <= 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||||
|
? value
|
||||||
|
: string.Empty;
|
||||||
|
|
||||||
|
var id = Interlocked.Increment(ref _nextPoolId);
|
||||||
|
_pools[id] = new NetPool(name, size, flags);
|
||||||
|
|
||||||
|
TraceNet("pool.create", id, unchecked((ulong)size), unchecked((ulong)flags), _initialized ? 1UL : 0UL);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)id);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "K7RlrTkI-mw",
|
||||||
|
ExportName = "sceNetPoolDestroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetPoolDestroy(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_pools.TryRemove(id, out _))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "C4UgDHHPvdw",
|
||||||
|
ExportName = "sceNetResolverCreate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetResolverCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var nameAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var poolId = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (flags != 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||||
|
? value
|
||||||
|
: string.Empty;
|
||||||
|
var id = Interlocked.Increment(ref _nextResolverId);
|
||||||
|
_resolvers[id] = new ResolverContext(name, poolId, flags, 0);
|
||||||
|
TraceNet("resolver.create", id, unchecked((ulong)poolId), unchecked((ulong)flags), _initialized ? 1UL : 0UL);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)id);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "kJlYH5uMAWI",
|
||||||
|
ExportName = "sceNetResolverDestroy",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetResolverDestroy(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
return _resolvers.TryRemove(id, out _)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "J5i3hiLJMPk",
|
||||||
|
ExportName = "sceNetResolverGetError",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNet")]
|
||||||
|
public static int NetResolverGetError(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var statusAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (statusAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_resolvers.TryGetValue(id, out var resolver))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> status = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
|
||||||
|
return ctx.Memory.TryWrite(statusAddress, status)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
if (address == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> one = stackalloc byte[1];
|
||||||
|
var bytes = new byte[maxLength];
|
||||||
|
var count = 0;
|
||||||
|
for (; count < maxLength; count++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)count, one))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (one[0] == 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[count] = one[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, count);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceNet(string operation, int id, ulong arg0, ulong arg1, ulong arg2)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NET"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] net.{operation} id={id} arg0=0x{arg0:X16} arg1=0x{arg1:X16} arg2=0x{arg2:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,87 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Network;
|
||||||
|
|
||||||
|
public static class SslExports
|
||||||
|
{
|
||||||
|
private const int SslErrorInvalidId = unchecked((int)0x8095F006);
|
||||||
|
private const int SslErrorOutOfSize = unchecked((int)0x8095F008);
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<int, SslContext> _contexts = new();
|
||||||
|
private static int _nextContextId;
|
||||||
|
|
||||||
|
private sealed record SslContext(ulong PoolSize);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "hdpVEUDFW3s",
|
||||||
|
ExportName = "sceSslInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSsl")]
|
||||||
|
public static int SslInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var poolSize = ctx[CpuRegister.Rdi];
|
||||||
|
if (poolSize == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, SslErrorOutOfSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = Interlocked.Increment(ref _nextContextId);
|
||||||
|
_contexts[id] = new SslContext(poolSize);
|
||||||
|
|
||||||
|
TraceSsl("init", id, poolSize);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)id);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0K1yQ6Lv-Yc",
|
||||||
|
ExportName = "sceSslTerm",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSsl")]
|
||||||
|
public static int SslTerm(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!_contexts.TryRemove(id, out _))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, SslErrorInvalidId);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSsl("term", id, 0);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "viRXSHZYd0c",
|
||||||
|
ExportName = "sceSslClose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSsl")]
|
||||||
|
public static int SslClose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
TraceSsl("close", id, 0);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceSsl(string operation, int id, ulong arg0)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SSL"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] ssl.{operation} id={id} arg0=0x{arg0:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Np;
|
||||||
|
|
||||||
|
public static class NpEntitlementAccessExports
|
||||||
|
{
|
||||||
|
private const int BootParamClearSize = 0x20;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "jO8DM8oyego",
|
||||||
|
ExportName = "sceNpEntitlementAccessInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpEntitlementAccess")]
|
||||||
|
public static int NpEntitlementAccessInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var initParam = ctx[CpuRegister.Rdi];
|
||||||
|
var bootParam = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
if (bootParam != 0)
|
||||||
|
{
|
||||||
|
Span<byte> clear = stackalloc byte[BootParamClearSize];
|
||||||
|
clear.Clear();
|
||||||
|
if (!ctx.Memory.TryWrite(bootParam, clear))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceNpEntitlementAccess(string message)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] np.entitlement.{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,143 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Np;
|
||||||
|
|
||||||
|
public static class NpManagerExports
|
||||||
|
{
|
||||||
|
private const int NpTitleIdSize = 16;
|
||||||
|
private const int NpTitleSecretSize = 128;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3Zl8BePTh9Y",
|
||||||
|
ExportName = "sceNpCheckCallback",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpCheckCallback(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JELHf4xPufo",
|
||||||
|
ExportName = "sceNpCheckCallbackForLib",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpCheckCallbackForLib(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "VfRSmPmj8Q8",
|
||||||
|
ExportName = "sceNpRegisterStateCallback",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpRegisterStateCallback(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "qQJfO8HAiaY",
|
||||||
|
ExportName = "sceNpRegisterStateCallbackA",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpRegisterStateCallbackA(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0c7HbXRKUt4",
|
||||||
|
ExportName = "sceNpRegisterStateCallbackForToolkit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManagerForToolkit")]
|
||||||
|
public static int NpRegisterStateCallbackForToolkit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "eQH7nWPcAgc",
|
||||||
|
ExportName = "sceNpGetState",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpGetState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (stateAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> stateBytes = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(stateBytes, 1);
|
||||||
|
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
||||||
|
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ec63y59l9tw",
|
||||||
|
ExportName = "sceNpSetNpTitleId",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpSetNpTitleId(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var titleIdAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var titleSecretAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (titleIdAddress == 0 || titleSecretAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> titleId = stackalloc byte[NpTitleIdSize];
|
||||||
|
Span<byte> titleSecret = stackalloc byte[NpTitleSecretSize];
|
||||||
|
if (!ctx.Memory.TryRead(titleIdAddress, titleId) ||
|
||||||
|
!ctx.Memory.TryRead(titleSecretAddress, titleSecret))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceNp($"set_np_title_id title='{ReadTitleId(titleId)}'");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
|
||||||
|
{
|
||||||
|
var length = 0;
|
||||||
|
while (length < 12 && length < bytes.Length && bytes[length] != 0)
|
||||||
|
{
|
||||||
|
length++;
|
||||||
|
}
|
||||||
|
|
||||||
|
return length == 0
|
||||||
|
? string.Empty
|
||||||
|
: System.Text.Encoding.ASCII.GetString(bytes[..length]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceNp(string message)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] np.{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Np;
|
||||||
|
|
||||||
|
public static class NpSessionSignalingExports
|
||||||
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ysmw6J-P8Ak",
|
||||||
|
ExportName = "sceNpSessionSignalingInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpSessionSignaling")]
|
||||||
|
public static int NpSessionSignalingInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Np;
|
||||||
|
|
||||||
|
public static class NpUniversalDataSystemExports
|
||||||
|
{
|
||||||
|
private const int NpUniversalDataSystemErrorInvalidArgument = unchecked((int)0x80553102);
|
||||||
|
private static int _nextHandle = 1;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sjaobBgqeB4",
|
||||||
|
ExportName = "sceNpUniversalDataSystemInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var parameterAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (parameterAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NpUniversalDataSystemErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> parameters = stackalloc byte[16];
|
||||||
|
return ctx.Memory.TryRead(parameterAddress, parameters)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "5zBnau1uIEo",
|
||||||
|
ExportName = "sceNpUniversalDataSystemCreateContext",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemCreateContext(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var contextAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (contextAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> context = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(context, 1);
|
||||||
|
return ctx.Memory.TryWrite(contextAddress, context)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "hT0IAEvN+M0",
|
||||||
|
ExportName = "sceNpUniversalDataSystemCreateHandle",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemCreateHandle(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = Interlocked.Increment(ref _nextHandle);
|
||||||
|
if (TryWriteInt32(ctx, ctx[CpuRegister.Rdi], handle) ||
|
||||||
|
TryWriteInt32(ctx, ctx[CpuRegister.Rsi], handle))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "tpFJ8LIKvPw",
|
||||||
|
ExportName = "sceNpUniversalDataSystemRegisterContext",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemRegisterContext(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||||
|
{
|
||||||
|
if (address == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System;
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Np;
|
||||||
|
|
||||||
|
public static class NpWebApi2Exports
|
||||||
|
{
|
||||||
|
private const int NpWebApi2ErrorInvalidArgument = unchecked((int)0x80553402);
|
||||||
|
|
||||||
|
private static int _initialized;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "+o9816YQhqQ",
|
||||||
|
ExportName = "sceNpWebApi2Initialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpWebApi2")]
|
||||||
|
public static int NpWebApi2Initialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var httpContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var poolSize = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
if (httpContextId <= 0 || poolSize == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, NpWebApi2ErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref _initialized, 1);
|
||||||
|
TraceNpWebApi2("init", httpContextId, poolSize);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "bEvXpcEk200",
|
||||||
|
ExportName = "sceNpWebApi2Terminate",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpWebApi2")]
|
||||||
|
public static int NpWebApi2Terminate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
Interlocked.Exchange(ref _initialized, 0);
|
||||||
|
TraceNpWebApi2("term", libraryContextId, 0);
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceNpWebApi2(string operation, int id, ulong arg0)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP_WEB_API2"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] npwebapi2.{operation} id={id} arg0=0x{arg0:X16} initialized={Volatile.Read(ref _initialized)}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.NpGameIntent;
|
||||||
|
|
||||||
|
public static class NpGameIntentExports
|
||||||
|
{
|
||||||
|
private static int _initialized;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "m87BHxt-H60",
|
||||||
|
ExportName = "sceNpGameIntentInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpGameIntent")]
|
||||||
|
public static int NpGameIntentInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Interlocked.Exchange(ref _initialized, 1);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -128,6 +128,38 @@ public static class PadExports
|
|||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
return WriteNeutralPadData(ctx, dataAddress)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "q1cHNfGycLI",
|
||||||
|
ExportName = "scePadRead",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePad")]
|
||||||
|
public static int PadRead(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var dataAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (handle != PrimaryPadHandle)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (dataAddress == 0 || count < 1 || count > 64)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return WriteNeutralPadData(ctx, dataAddress)
|
||||||
|
? SetReturn(ctx, 1)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
|
||||||
|
{
|
||||||
Span<byte> data = stackalloc byte[PadDataSize];
|
Span<byte> data = stackalloc byte[PadDataSize];
|
||||||
data.Clear();
|
data.Clear();
|
||||||
data[0x04] = 128;
|
data[0x04] = 128;
|
||||||
@@ -145,9 +177,7 @@ public static class PadExports
|
|||||||
timestampMicroseconds);
|
timestampMicroseconds);
|
||||||
data[0x68] = 1;
|
data[0x68] = 1;
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(dataAddress, data)
|
return ctx.Memory.TryWrite(dataAddress, data);
|
||||||
? SetReturn(ctx, 0)
|
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Text.RegularExpressions;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.PlayGo;
|
namespace SharpEmu.Libs.PlayGo;
|
||||||
|
|
||||||
@@ -14,15 +15,38 @@ public static class PlayGoExports
|
|||||||
private const int OrbisPlayGoErrorBadHandle = unchecked((int)0x80B20009);
|
private const int OrbisPlayGoErrorBadHandle = unchecked((int)0x80B20009);
|
||||||
private const int OrbisPlayGoErrorBadPointer = unchecked((int)0x80B2000A);
|
private const int OrbisPlayGoErrorBadPointer = unchecked((int)0x80B2000A);
|
||||||
private const int OrbisPlayGoErrorBadSize = unchecked((int)0x80B2000B);
|
private const int OrbisPlayGoErrorBadSize = unchecked((int)0x80B2000B);
|
||||||
|
private const int OrbisPlayGoErrorBadChunkId = unchecked((int)0x80B2000C);
|
||||||
private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E);
|
private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E);
|
||||||
|
private const int OrbisPlayGoErrorBadLocus = unchecked((int)0x80B20010);
|
||||||
private const ulong PlayGoInitBufAddrOffset = 0;
|
private const ulong PlayGoInitBufAddrOffset = 0;
|
||||||
private const ulong PlayGoInitBufSizeOffset = 8;
|
private const ulong PlayGoInitBufSizeOffset = 8;
|
||||||
private const uint PlayGoMinimumInitBufferSize = 0x200000;
|
private const uint PlayGoMinimumInitBufferSize = 0x200000;
|
||||||
private const uint PlayGoHandle = 1;
|
private const uint PlayGoHandle = 1;
|
||||||
|
private const int PlayGoLocusNotDownloaded = 0;
|
||||||
|
private const int PlayGoLocusLocalSlow = 2;
|
||||||
|
private const int PlayGoLocusLocalFast = 3;
|
||||||
|
private const int PlayGoInstallSpeedSuspended = 0;
|
||||||
|
private const int PlayGoInstallSpeedTrickle = 1;
|
||||||
|
private const int PlayGoInstallSpeedFull = 2;
|
||||||
|
private const uint MaxPlayGoQueryEntries = 0x4000;
|
||||||
|
private const uint PlayGoAllEntriesSentinel = uint.MaxValue;
|
||||||
|
|
||||||
|
private static readonly Regex ChunkIdPattern = new(
|
||||||
|
@"<chunk\s+[^>]*\bid\s*=\s*""(?<id>\d+)""",
|
||||||
|
RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
|
private static readonly Regex DefaultChunkPattern = new(
|
||||||
|
@"default_chunk\s*=\s*""(?<id>\d+)""",
|
||||||
|
RegexOptions.CultureInvariant);
|
||||||
|
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static bool _initialized;
|
private static bool _initialized;
|
||||||
private static bool _hasPlayGoData;
|
private static bool _opened;
|
||||||
|
private static PlayGoMetadata _metadata = PlayGoMetadata.Empty;
|
||||||
|
private static int _installSpeed = PlayGoInstallSpeedTrickle;
|
||||||
|
private static ulong _languageMask = ulong.MaxValue;
|
||||||
|
private static int _unknownChunkDiagnostics;
|
||||||
|
private static int _locusTraceDiagnostics;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ts6GlZOKRrE",
|
Nid = "ts6GlZOKRrE",
|
||||||
@@ -67,8 +91,12 @@ public static class PlayGoExports
|
|||||||
return OrbisPlayGoErrorAlreadyInitialized;
|
return OrbisPlayGoErrorAlreadyInitialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
_hasPlayGoData = HasPlayGoChunkData();
|
_metadata = LoadPlayGoMetadata();
|
||||||
|
_installSpeed = PlayGoInstallSpeedTrickle;
|
||||||
|
_languageMask = ulong.MaxValue;
|
||||||
|
_opened = false;
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
|
TracePlayGo($"initialize chunks={_metadata.ChunkIds.Length} available={_metadata.Available}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -100,10 +128,13 @@ public static class PlayGoExports
|
|||||||
return OrbisPlayGoErrorNotInitialized;
|
return OrbisPlayGoErrorNotInitialized;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!_hasPlayGoData)
|
if (!_metadata.Available)
|
||||||
{
|
{
|
||||||
return OrbisPlayGoErrorNotSupportPlayGo;
|
return OrbisPlayGoErrorNotSupportPlayGo;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_opened = true;
|
||||||
|
TracePlayGo($"open handle={PlayGoHandle} chunks={_metadata.ChunkIds.Length}");
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
|
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
|
||||||
@@ -132,7 +163,8 @@ public static class PlayGoExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_initialized = false;
|
_initialized = false;
|
||||||
_hasPlayGoData = false;
|
_opened = false;
|
||||||
|
_metadata = PlayGoMetadata.Empty;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -157,20 +189,611 @@ public static class PlayGoExports
|
|||||||
{
|
{
|
||||||
return OrbisPlayGoErrorBadHandle;
|
return OrbisPlayGoErrorBadHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_opened = false;
|
||||||
}
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasPlayGoChunkData()
|
[SysAbiExport(
|
||||||
|
Nid = "73fF1MFU8hA",
|
||||||
|
ExportName = "scePlayGoGetChunkId",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePlayGo")]
|
||||||
|
public static int PlayGoGetChunkId(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var outChunkIdList = ctx[CpuRegister.Rsi];
|
||||||
|
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
var outEntries = ctx[CpuRegister.Rcx];
|
||||||
|
|
||||||
|
var validation = ValidateHandle(handle);
|
||||||
|
if (validation != 0)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outEntries == 0)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outChunkIdList != 0 && numberOfEntries == 0)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
ushort[] chunkIds;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
chunkIds = _metadata.ChunkIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length;
|
||||||
|
if (outChunkIdList == 0)
|
||||||
|
{
|
||||||
|
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
|
||||||
|
return TryWriteUInt32(ctx, outEntries, availableEntries)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entriesToWrite = Math.Min(numberOfEntries, availableEntries);
|
||||||
|
if (entriesToWrite > MaxPlayGoQueryEntries)
|
||||||
|
{
|
||||||
|
TracePlayGo($"get_chunk_id bad_size requested={numberOfEntries} available={availableEntries}");
|
||||||
|
return OrbisPlayGoErrorBadSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint i = 0; i < entriesToWrite; i++)
|
||||||
|
{
|
||||||
|
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
|
||||||
|
if (!TryWriteUInt16(ctx, outChunkIdList + (i * sizeof(ushort)), chunkId))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePlayGo($"get_chunk_id write requested={numberOfEntries} wrote={entriesToWrite} available={availableEntries}");
|
||||||
|
return TryWriteUInt32(ctx, outEntries, entriesToWrite)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "v6EZ-YWRdMs",
|
||||||
|
ExportName = "scePlayGoGetEta",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePlayGo")]
|
||||||
|
public static int PlayGoGetEta(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var chunkIds = ctx[CpuRegister.Rsi];
|
||||||
|
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
var outEta = ctx[CpuRegister.Rcx];
|
||||||
|
|
||||||
|
var validation = ValidateHandle(handle);
|
||||||
|
if (validation != 0)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkIds == 0 || outEta == 0)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
|
||||||
|
{
|
||||||
|
TracePlayGo($"get_eta bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outEta:X16}");
|
||||||
|
return OrbisPlayGoErrorBadSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0
|
||||||
|
? chunkError
|
||||||
|
: TryWriteInt64(ctx, outEta, 0)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rvBSfTimejE",
|
||||||
|
ExportName = "scePlayGoGetInstallSpeed",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePlayGo")]
|
||||||
|
public static int PlayGoGetInstallSpeed(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var outSpeed = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
var validation = ValidateHandle(handle);
|
||||||
|
if (validation != 0)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outSpeed == 0)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
int speed;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
speed = _installSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryWriteInt32(ctx, outSpeed, speed)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3OMbYZBaa50",
|
||||||
|
ExportName = "scePlayGoGetLanguageMask",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePlayGo")]
|
||||||
|
public static int PlayGoGetLanguageMask(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var outLanguageMask = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
var validation = ValidateHandle(handle);
|
||||||
|
if (validation != 0)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outLanguageMask == 0)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong languageMask;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
languageMask = _languageMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.TryWriteUInt64(outLanguageMask, languageMask)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uWIYLFkkwqk",
|
||||||
|
ExportName = "scePlayGoGetLocus",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePlayGo")]
|
||||||
|
public static int PlayGoGetLocus(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var chunkIds = ctx[CpuRegister.Rsi];
|
||||||
|
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
var outLoci = ctx[CpuRegister.Rcx];
|
||||||
|
|
||||||
|
var validation = ValidateHandle(handle);
|
||||||
|
if (validation != 0)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkIds == 0 || outLoci == 0)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numberOfEntries == PlayGoAllEntriesSentinel)
|
||||||
|
{
|
||||||
|
TracePlayGo($"get_locus sentinel entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
|
||||||
|
{
|
||||||
|
TracePlayGo($"get_locus bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
|
||||||
|
return OrbisPlayGoErrorBadSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
var loci = new byte[numberOfEntries];
|
||||||
|
for (uint i = 0; i < numberOfEntries; i++)
|
||||||
|
{
|
||||||
|
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!IsKnownChunkId(chunkId))
|
||||||
|
{
|
||||||
|
if (Interlocked.Increment(ref _unknownChunkDiagnostics) <= 8)
|
||||||
|
{
|
||||||
|
ushort[] knownChunkIds;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
knownChunkIds = _metadata.ChunkIds;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] playgo.unknown_chunk_id id={chunkId} entries={numberOfEntries} " +
|
||||||
|
$"known=[{string.Join(',', knownChunkIds)}]");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
loci[i] = PlayGoLocusLocalFast;
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePlayGoLocus(numberOfEntries, chunkIds, outLoci);
|
||||||
|
return ctx.Memory.TryWrite(outLoci, loci)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-RJWNMK3fC8",
|
||||||
|
ExportName = "scePlayGoGetProgress",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePlayGo")]
|
||||||
|
public static int PlayGoGetProgress(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var chunkIds = ctx[CpuRegister.Rsi];
|
||||||
|
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
var outProgress = ctx[CpuRegister.Rcx];
|
||||||
|
|
||||||
|
var validation = ValidateHandle(handle);
|
||||||
|
if (validation != 0)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkIds == 0 || outProgress == 0)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
|
||||||
|
{
|
||||||
|
TracePlayGo($"get_progress bad_size entries={numberOfEntries} chunk_ids=0x{chunkIds:X16} out=0x{outProgress:X16}");
|
||||||
|
return OrbisPlayGoErrorBadSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunkError = ValidateChunkIds(ctx, chunkIds, numberOfEntries);
|
||||||
|
if (chunkError != 0)
|
||||||
|
{
|
||||||
|
return chunkError;
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePlayGo($"get_progress entries={numberOfEntries}");
|
||||||
|
Span<byte> progress = stackalloc byte[sizeof(ulong) * 2];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(progress, 0);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(progress[sizeof(ulong)..], 0);
|
||||||
|
return ctx.Memory.TryWrite(outProgress, progress)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Nn7zKwnA5q0",
|
||||||
|
ExportName = "scePlayGoGetToDoList",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePlayGo")]
|
||||||
|
public static int PlayGoGetToDoList(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var outTodoList = ctx[CpuRegister.Rsi];
|
||||||
|
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
var outEntries = ctx[CpuRegister.Rcx];
|
||||||
|
|
||||||
|
var validation = ValidateHandle(handle);
|
||||||
|
if (validation != 0)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (outTodoList == 0 || outEntries == 0)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numberOfEntries == 0)
|
||||||
|
{
|
||||||
|
TracePlayGo("get_todo bad_size entries=0");
|
||||||
|
return OrbisPlayGoErrorBadSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePlayGo($"get_todo requested={numberOfEntries} wrote=0");
|
||||||
|
return TryWriteUInt32(ctx, outEntries, 0)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-Q1-u1a7p0g",
|
||||||
|
ExportName = "scePlayGoPrefetch",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePlayGo")]
|
||||||
|
public static int PlayGoPrefetch(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var chunkIds = ctx[CpuRegister.Rsi];
|
||||||
|
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
var minimumLocus = unchecked((int)ctx[CpuRegister.Rcx]);
|
||||||
|
|
||||||
|
var validation = ValidateHandle(handle);
|
||||||
|
if (validation != 0)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunkIds == 0)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (numberOfEntries == 0 || numberOfEntries > MaxPlayGoQueryEntries)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadSize;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (minimumLocus is not PlayGoLocusNotDownloaded and not PlayGoLocusLocalSlow and not PlayGoLocusLocalFast)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadLocus;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0
|
||||||
|
? chunkError
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "4AAcTU9R3XM",
|
||||||
|
ExportName = "scePlayGoSetInstallSpeed",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePlayGo")]
|
||||||
|
public static int PlayGoSetInstallSpeed(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var speed = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
|
||||||
|
var validation = ValidateHandle(handle);
|
||||||
|
if (validation != 0)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (speed is not PlayGoInstallSpeedSuspended and not PlayGoInstallSpeedTrickle and not PlayGoInstallSpeedFull)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorInvalidArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_installSpeed = speed;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "LosLlHOpNqQ",
|
||||||
|
ExportName = "scePlayGoSetLanguageMask",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePlayGo")]
|
||||||
|
public static int PlayGoSetLanguageMask(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var languageMask = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
var validation = ValidateHandle(handle);
|
||||||
|
if (validation != 0)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_languageMask = languageMask;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gUPGiOQ1tmQ",
|
||||||
|
ExportName = "scePlayGoSetToDoList",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePlayGo")]
|
||||||
|
public static int PlayGoSetToDoList(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
|
var todoList = ctx[CpuRegister.Rsi];
|
||||||
|
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||||
|
|
||||||
|
var validation = ValidateHandle(handle);
|
||||||
|
if (validation != 0)
|
||||||
|
{
|
||||||
|
return validation;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (todoList == 0)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadPointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
return numberOfEntries == 0
|
||||||
|
? OrbisPlayGoErrorBadSize
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ValidateHandle(uint handle)
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_initialized)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorNotInitialized;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (handle != PlayGoHandle || !_opened)
|
||||||
|
{
|
||||||
|
return OrbisPlayGoErrorBadHandle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ValidateChunkIds(CpuContext ctx, ulong chunkIds, uint numberOfEntries)
|
||||||
|
{
|
||||||
|
for (uint i = 0; i < numberOfEntries; i++)
|
||||||
|
{
|
||||||
|
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = chunkId;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsKnownChunkId(ushort chunkId)
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
return _metadata.ChunkIds.Length == 0 || Array.BinarySearch(_metadata.ChunkIds, chunkId) >= 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PlayGoMetadata LoadPlayGoMetadata()
|
||||||
{
|
{
|
||||||
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||||
if (string.IsNullOrWhiteSpace(app0Root))
|
if (string.IsNullOrWhiteSpace(app0Root))
|
||||||
{
|
{
|
||||||
|
return PlayGoMetadata.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
||||||
|
var scenarioJson = Path.Combine(app0Root, "sce_sys", "playgo-scenario.json");
|
||||||
|
var chunkDefsXml = Path.Combine(app0Root, "playgo-chunkdefs.xml");
|
||||||
|
|
||||||
|
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
||||||
|
if (!hasMetadata)
|
||||||
|
{
|
||||||
|
return PlayGoMetadata.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunkIds = LoadChunkIds(chunkDefsXml);
|
||||||
|
return new PlayGoMetadata(true, chunkIds);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
||||||
|
{
|
||||||
|
if (!File.Exists(chunkDefsXml))
|
||||||
|
{
|
||||||
|
return Array.Empty<ushort>();
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var xml = File.ReadAllText(chunkDefsXml);
|
||||||
|
var chunkIds = new HashSet<ushort>();
|
||||||
|
AddChunkIds(xml, DefaultChunkPattern, chunkIds);
|
||||||
|
AddChunkIds(xml, ChunkIdPattern, chunkIds);
|
||||||
|
|
||||||
|
var sorted = chunkIds.ToArray();
|
||||||
|
Array.Sort(sorted);
|
||||||
|
return sorted;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return Array.Empty<ushort>();
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return Array.Empty<ushort>();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void AddChunkIds(string xml, Regex pattern, HashSet<ushort> chunkIds)
|
||||||
|
{
|
||||||
|
foreach (Match match in pattern.Matches(xml))
|
||||||
|
{
|
||||||
|
if (ushort.TryParse(match.Groups["id"].Value, out var chunkId))
|
||||||
|
{
|
||||||
|
chunkIds.Add(chunkId);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var hostPath = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
value = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
|
||||||
return File.Exists(hostPath);
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteInt64(CpuContext ctx, ulong address, long value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(long)];
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TracePlayGo(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] playgo.{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TracePlayGoLocus(uint entries, ulong chunkIds, ulong outLoci)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var count = Interlocked.Increment(ref _locusTraceDiagnostics);
|
||||||
|
if (entries != 1 || count <= 32 || count % 1000 == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] playgo.get_locus entries={entries} chunk_ids=0x{chunkIds:X16} out=0x{outLoci:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private sealed record PlayGoMetadata(bool Available, ushort[] ChunkIds)
|
||||||
|
{
|
||||||
|
public static readonly PlayGoMetadata Empty = new(false, Array.Empty<ushort>());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -8,6 +8,8 @@ namespace SharpEmu.Libs.Rtc;
|
|||||||
|
|
||||||
public static class RtcExports
|
public static class RtcExports
|
||||||
{
|
{
|
||||||
|
private const long DateTimeTicksPerMicrosecond = 10;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ZPD1YOKI+Kw",
|
Nid = "ZPD1YOKI+Kw",
|
||||||
ExportName = "sceRtcGetCurrentClockLocalTime",
|
ExportName = "sceRtcGetCurrentClockLocalTime",
|
||||||
@@ -42,6 +44,29 @@ public static class RtcExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "18B2NS1y9UU",
|
||||||
|
ExportName = "sceRtcGetCurrentTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetCurrentTick(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var tickAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (tickAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
|
||||||
|
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "8w-H19ip48I",
|
Nid = "8w-H19ip48I",
|
||||||
ExportName = "sceRtcGetTick",
|
ExportName = "sceRtcGetTick",
|
||||||
@@ -72,14 +97,57 @@ public static class RtcExports
|
|||||||
rtcDateTime.Minute,
|
rtcDateTime.Minute,
|
||||||
rtcDateTime.Second,
|
rtcDateTime.Second,
|
||||||
DateTimeKind.Utc);
|
DateTimeKind.Utc);
|
||||||
tickValue = checked((ulong)((baseDateTime.Ticks / 10) + rtcDateTime.Microsecond));
|
tickValue = checked((ulong)((baseDateTime.Ticks / DateTimeTicksPerMicrosecond) + rtcDateTime.Microsecond));
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is ArgumentOutOfRangeException or OverflowException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ueega6v3GUw",
|
||||||
|
ExportName = "sceRtcSetTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcSetTick(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var dateTimeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var tickAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (dateTimeAddress == 0 || tickAddress == 0)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(tickAddress, out var tickValue))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tickValue > long.MaxValue / DateTimeTicksPerMicrosecond)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime dateTime;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dateTime = new DateTime(checked((long)tickValue * DateTimeTicksPerMicrosecond), DateTimeKind.Utc);
|
||||||
}
|
}
|
||||||
catch (ArgumentOutOfRangeException)
|
catch (ArgumentOutOfRangeException)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
if (!TryWriteRtcDateTime(ctx, dateTimeAddress, dateTime))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -108,6 +176,21 @@ public static class RtcExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, DateTime dateTime)
|
||||||
|
{
|
||||||
|
Span<byte> rtcDateTime = stackalloc byte[16];
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)dateTime.Year));
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)dateTime.Month));
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)dateTime.Day));
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)dateTime.Hour));
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)dateTime.Minute));
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)dateTime.Second));
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||||
|
rtcDateTime[12..16],
|
||||||
|
checked((uint)((dateTime.Ticks % TimeSpan.TicksPerSecond) / DateTimeTicksPerMicrosecond)));
|
||||||
|
return ctx.Memory.TryWrite(address, rtcDateTime);
|
||||||
|
}
|
||||||
|
|
||||||
private readonly record struct RtcDateTime(
|
private readonly record struct RtcDateTime(
|
||||||
ushort Year,
|
ushort Year,
|
||||||
ushort Month,
|
ushort Month,
|
||||||
|
|||||||
@@ -0,0 +1,444 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.SaveData;
|
||||||
|
|
||||||
|
public static class SaveDataExports
|
||||||
|
{
|
||||||
|
private const int OrbisSaveDataErrorParameter = unchecked((int)0x809F0000);
|
||||||
|
private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B);
|
||||||
|
private const int SaveDataTitleIdSize = 10;
|
||||||
|
private const int SaveDataDirNameSize = 32;
|
||||||
|
private const int SaveDataParamSize = 0x530;
|
||||||
|
private const int SaveDataSearchInfoSize = 0x30;
|
||||||
|
private const ulong ResultHitNumOffset = 0x00;
|
||||||
|
private const ulong ResultDirNamesOffset = 0x08;
|
||||||
|
private const ulong ResultDirNamesNumOffset = 0x10;
|
||||||
|
private const ulong ResultSetNumOffset = 0x14;
|
||||||
|
private const ulong ResultParamsOffset = 0x18;
|
||||||
|
private const ulong ResultInfosOffset = 0x20;
|
||||||
|
private const uint SortKeyFreeBlocks = 5;
|
||||||
|
private const uint SortOrderDescent = 1;
|
||||||
|
private static readonly object _stateGate = new();
|
||||||
|
private static string? _titleId;
|
||||||
|
|
||||||
|
public static void ConfigureApplicationInfo(string? titleId)
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "TywrFKCoLGY",
|
||||||
|
ExportName = "sceSaveDataInitialize3",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataInitialize3(CpuContext ctx)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(ResolveSaveDataRoot());
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "dyIhnXq-0SM",
|
||||||
|
ExportName = "sceSaveDataDirNameSearch",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSaveData")]
|
||||||
|
public static int SaveDataDirNameSearch(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var condAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var resultAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (condAddress == 0 || resultAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
|
||||||
|
!TryReadSearchResult(ctx, resultAddress, out var result))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
string titleId;
|
||||||
|
if (cond.TitleIdAddress == 0)
|
||||||
|
{
|
||||||
|
titleId = ResolveConfiguredTitleId();
|
||||||
|
}
|
||||||
|
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
|
||||||
|
var entries = Directory.Exists(root)
|
||||||
|
? EnumerateSaveDirectories(root, cond.Pattern)
|
||||||
|
: [];
|
||||||
|
|
||||||
|
entries = SortEntries(entries, cond.SortKey, cond.SortOrder);
|
||||||
|
var setNum = result.DirNamesNum == 0
|
||||||
|
? 0
|
||||||
|
: Math.Min(result.DirNamesNum, entries.Count);
|
||||||
|
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
||||||
|
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (setNum == 0)
|
||||||
|
{
|
||||||
|
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (result.DirNamesAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < setNum; i++)
|
||||||
|
{
|
||||||
|
var entry = entries[i];
|
||||||
|
if (!TryWriteFixedAscii(
|
||||||
|
ctx,
|
||||||
|
result.DirNamesAddress + ((ulong)i * SaveDataDirNameSize),
|
||||||
|
SaveDataDirNameSize,
|
||||||
|
entry.Name) ||
|
||||||
|
(result.ParamsAddress != 0 &&
|
||||||
|
!TryWriteParam(ctx, result.ParamsAddress + ((ulong)i * SaveDataParamSize), entry)) ||
|
||||||
|
(result.InfosAddress != 0 &&
|
||||||
|
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
|
}
|
||||||
|
catch (UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
||||||
|
{
|
||||||
|
cond = default;
|
||||||
|
if (!TryReadInt32(ctx, address, out var userId) ||
|
||||||
|
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
||||||
|
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
||||||
|
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
|
||||||
|
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
string pattern;
|
||||||
|
if (dirNameAddress == 0)
|
||||||
|
{
|
||||||
|
pattern = string.Empty;
|
||||||
|
}
|
||||||
|
else if (!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out pattern))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cond = new SearchCond(userId, titleIdAddress, pattern, sortKey, sortOrder);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadSearchResult(CpuContext ctx, ulong address, out SearchResult result)
|
||||||
|
{
|
||||||
|
result = default;
|
||||||
|
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
|
||||||
|
!TryReadUInt32(ctx, address + ResultDirNamesNumOffset, out var dirNamesNum) ||
|
||||||
|
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
|
||||||
|
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = new SearchResult(dirNamesAddress, dirNamesNum, paramsAddress, infosAddress);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<SaveEntry> EnumerateSaveDirectories(string root, string pattern)
|
||||||
|
{
|
||||||
|
var entries = new List<SaveEntry>();
|
||||||
|
foreach (var directory in Directory.EnumerateDirectories(root))
|
||||||
|
{
|
||||||
|
var name = Path.GetFileName(directory);
|
||||||
|
if (string.IsNullOrWhiteSpace(name) ||
|
||||||
|
name.StartsWith("sce_", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(!string.IsNullOrEmpty(pattern) && !MatchPattern(name, pattern)))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var info = new DirectoryInfo(directory);
|
||||||
|
entries.Add(new SaveEntry(name, directory, info.LastWriteTimeUtc));
|
||||||
|
}
|
||||||
|
|
||||||
|
return entries;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static List<SaveEntry> SortEntries(List<SaveEntry> entries, uint sortKey, uint sortOrder)
|
||||||
|
{
|
||||||
|
IOrderedEnumerable<SaveEntry> sorted = sortKey switch
|
||||||
|
{
|
||||||
|
3 => entries.OrderBy(entry => entry.LastWriteUtc),
|
||||||
|
_ => entries.OrderBy(entry => entry.Name, StringComparer.OrdinalIgnoreCase),
|
||||||
|
};
|
||||||
|
|
||||||
|
var list = sorted.ToList();
|
||||||
|
if (sortOrder == SortOrderDescent)
|
||||||
|
{
|
||||||
|
list.Reverse();
|
||||||
|
}
|
||||||
|
|
||||||
|
return list;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteParam(CpuContext ctx, ulong address, SaveEntry entry)
|
||||||
|
{
|
||||||
|
var param = new byte[SaveDataParamSize];
|
||||||
|
WriteAscii(param.AsSpan(0x00, 128), "Saved Data");
|
||||||
|
WriteAscii(param.AsSpan(0x100, 1024), entry.Name);
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(
|
||||||
|
param.AsSpan(0x508, sizeof(long)),
|
||||||
|
new DateTimeOffset(entry.LastWriteUtc).ToUnixTimeSeconds());
|
||||||
|
return ctx.Memory.TryWrite(address, param);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteSearchInfo(CpuContext ctx, ulong address, SaveEntry entry)
|
||||||
|
{
|
||||||
|
var size = GetDirectorySize(entry.Path);
|
||||||
|
var usedBlocks = checked((ulong)((size + 32767) / 32768));
|
||||||
|
var blocks = Math.Max(96UL, usedBlocks);
|
||||||
|
Span<byte> info = stackalloc byte[SaveDataSearchInfoSize];
|
||||||
|
info.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(info[0x00..], blocks);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(info[0x08..], blocks - usedBlocks);
|
||||||
|
return ctx.Memory.TryWrite(address, info);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static long GetDirectorySize(string root)
|
||||||
|
{
|
||||||
|
long total = 0;
|
||||||
|
foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories))
|
||||||
|
{
|
||||||
|
total += new FileInfo(file).Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool MatchPattern(string value, string pattern) =>
|
||||||
|
MatchPattern(value.AsSpan(), pattern.AsSpan());
|
||||||
|
|
||||||
|
private static bool MatchPattern(ReadOnlySpan<char> value, ReadOnlySpan<char> pattern)
|
||||||
|
{
|
||||||
|
if (pattern.IsEmpty)
|
||||||
|
{
|
||||||
|
return value.IsEmpty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pattern[0] == '%')
|
||||||
|
{
|
||||||
|
for (var i = 0; i <= value.Length; i++)
|
||||||
|
{
|
||||||
|
if (MatchPattern(value[i..], pattern[1..]))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (value.IsEmpty)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pattern[0] == '_' ||
|
||||||
|
char.ToUpperInvariant(pattern[0]) == char.ToUpperInvariant(value[0]))
|
||||||
|
{
|
||||||
|
return MatchPattern(value[1..], pattern[1..]);
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveTitleSaveRoot(int userId, string titleId) =>
|
||||||
|
Path.Combine(ResolveSaveDataRoot(), userId.ToString(), SanitizePathSegment(titleId));
|
||||||
|
|
||||||
|
private static string ResolveSaveDataRoot()
|
||||||
|
{
|
||||||
|
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
||||||
|
var root = string.IsNullOrWhiteSpace(configured)
|
||||||
|
? Path.Combine(Environment.CurrentDirectory, "user", "savedata")
|
||||||
|
: configured;
|
||||||
|
return Path.GetFullPath(root);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string ResolveConfiguredTitleId()
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(_titleId))
|
||||||
|
{
|
||||||
|
return _titleId;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||||
|
var app0Name = string.IsNullOrWhiteSpace(app0Root)
|
||||||
|
? null
|
||||||
|
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
|
||||||
|
if (!string.IsNullOrWhiteSpace(app0Name))
|
||||||
|
{
|
||||||
|
var candidate = app0Name.Split('-', StringSplitOptions.RemoveEmptyEntries)[0];
|
||||||
|
if (!string.IsNullOrWhiteSpace(candidate))
|
||||||
|
{
|
||||||
|
return SanitizePathSegment(candidate);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return "default";
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string SanitizePathSegment(string value)
|
||||||
|
{
|
||||||
|
var invalid = Path.GetInvalidFileNameChars();
|
||||||
|
var sanitized = new string(value.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray());
|
||||||
|
return string.IsNullOrWhiteSpace(sanitized) ? "default" : sanitized;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadFixedAscii(CpuContext ctx, ulong address, int length, out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
Span<byte> buffer = stackalloc byte[length];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stringLength = buffer.IndexOf((byte)0);
|
||||||
|
if (stringLength < 0)
|
||||||
|
{
|
||||||
|
stringLength = buffer.Length;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.ASCII.GetString(buffer[..stringLength]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteFixedAscii(CpuContext ctx, ulong address, int length, string value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[length];
|
||||||
|
buffer.Clear();
|
||||||
|
WriteAscii(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteAscii(Span<byte> destination, string value)
|
||||||
|
{
|
||||||
|
var count = Math.Min(value.Length, Math.Max(0, destination.Length - 1));
|
||||||
|
for (var i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
var ch = value[i];
|
||||||
|
destination[i] = ch <= 0x7F ? (byte)ch : (byte)'?';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceSaveData(string message)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] savedata.{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private readonly record struct SearchCond(
|
||||||
|
int UserId,
|
||||||
|
ulong TitleIdAddress,
|
||||||
|
string Pattern,
|
||||||
|
uint SortKey,
|
||||||
|
uint SortOrder);
|
||||||
|
|
||||||
|
private readonly record struct SearchResult(
|
||||||
|
ulong DirNamesAddress,
|
||||||
|
uint DirNamesNum,
|
||||||
|
ulong ParamsAddress,
|
||||||
|
ulong InfosAddress);
|
||||||
|
|
||||||
|
private readonly record struct SaveEntry(string Name, string Path, DateTime LastWriteUtc);
|
||||||
|
}
|
||||||
@@ -0,0 +1,112 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Share;
|
||||||
|
|
||||||
|
public static class ShareExports
|
||||||
|
{
|
||||||
|
private const int MaxContentParamBytes = 4096;
|
||||||
|
|
||||||
|
private static int _initialized;
|
||||||
|
private static string _contentParam = string.Empty;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "nBDD66kiFW8",
|
||||||
|
ExportName = "sceShareInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceShareUtility")]
|
||||||
|
public static int ShareInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var memorySize = ctx[CpuRegister.Rdi];
|
||||||
|
var priority = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var affinityMask = ctx[CpuRegister.Rdx];
|
||||||
|
if (memorySize == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Interlocked.Exchange(ref _initialized, 1);
|
||||||
|
|
||||||
|
TraceShare($"initialize memory=0x{memorySize:X} priority={priority} affinity=0x{affinityMask:X}");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "7QZtURYnXG4",
|
||||||
|
ExportName = "sceShareSetContentParam",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceShareUtility")]
|
||||||
|
public static int ShareSetContentParam(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var contentParamAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (contentParamAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadNullTerminatedUtf8(ctx, contentParamAddress, MaxContentParamBytes, out var contentParam))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
_contentParam = contentParam;
|
||||||
|
if (Volatile.Read(ref _initialized) == 0)
|
||||||
|
{
|
||||||
|
TraceShare("set_content_param before initialize");
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceShare($"set_content_param len={contentParam.Length} preview='{FormatTraceString(contentParam)}'");
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[maxLength];
|
||||||
|
Span<byte> one = stackalloc byte[1];
|
||||||
|
for (var index = 0; index < maxLength; index++)
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)index, one))
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (one[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes[..index]);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[index] = one[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string FormatTraceString(string value)
|
||||||
|
{
|
||||||
|
var normalized = value.Replace("\r", "\\r", StringComparison.Ordinal).Replace("\n", "\\n", StringComparison.Ordinal);
|
||||||
|
return normalized.Length <= 120 ? normalized : string.Concat(normalized.AsSpan(0, 120), "...");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceShare(string message)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SHARE"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] share.{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.SystemGesture;
|
||||||
|
|
||||||
|
public static class SystemGestureExports
|
||||||
|
{
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3pcAvmwKCvM",
|
||||||
|
ExportName = "sceSystemGestureInitializePrimitiveTouchRecognizer",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemGesture")]
|
||||||
|
public static int SystemGestureInitializePrimitiveTouchRecognizer(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "FWF8zkhr854",
|
||||||
|
ExportName = "sceSystemGestureCreateTouchRecognizer",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemGesture")]
|
||||||
|
public static int SystemGestureCreateTouchRecognizer(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "qpo-mEOwje0",
|
||||||
|
ExportName = "sceSystemGestureOpen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemGesture")]
|
||||||
|
public static int SystemGestureOpen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "GgFMb22sbbI",
|
||||||
|
ExportName = "sceSystemGestureUpdatePrimitiveTouchRecognizer",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemGesture")]
|
||||||
|
public static int SystemGestureUpdatePrimitiveTouchRecognizer(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "j4h82CQWENo",
|
||||||
|
ExportName = "sceSystemGestureUpdateTouchRecognizer",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemGesture")]
|
||||||
|
public static int SystemGestureUpdateTouchRecognizer(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "h8uongcBNVs",
|
||||||
|
ExportName = "sceSystemGestureGetTouchEventsCount",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemGesture")]
|
||||||
|
public static int SystemGestureGetTouchEventsCount(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.VideoOut;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.SystemService;
|
namespace SharpEmu.Libs.SystemService;
|
||||||
@@ -10,6 +11,35 @@ public static class SystemServiceExports
|
|||||||
{
|
{
|
||||||
private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003);
|
private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003);
|
||||||
private const int SystemServiceStatusSize = 0x0C;
|
private const int SystemServiceStatusSize = 0x0C;
|
||||||
|
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "fZo48un7LK4",
|
||||||
|
ExportName = "sceSystemServiceParamGetInt",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemService")]
|
||||||
|
public static int SystemServiceParamGetInt(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var parameterId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (valueAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
var value = parameterId switch
|
||||||
|
{
|
||||||
|
1 or 2 or 3 or 1000 => 1,
|
||||||
|
4 => 180,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
|
||||||
|
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||||
|
return ctx.Memory.TryWrite(valueAddress, valueBytes)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "rPo6tV8D9bM",
|
Nid = "rPo6tV8D9bM",
|
||||||
@@ -34,6 +64,39 @@ public static class SystemServiceExports
|
|||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1n37q1Bvc5Y",
|
||||||
|
ExportName = "sceSystemServiceGetDisplaySafeAreaInfo",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemService")]
|
||||||
|
public static int SystemServiceGetDisplaySafeAreaInfo(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var infoAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (infoAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> info = stackalloc byte[DisplaySafeAreaInfoSize];
|
||||||
|
info.Clear();
|
||||||
|
BinaryPrimitives.WriteSingleLittleEndian(info, 1.0f);
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(infoAddress, info)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Vo5V8KAwCmk",
|
||||||
|
ExportName = "sceSystemServiceHideSplashScreen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemService")]
|
||||||
|
public static int SystemServiceHideSplashScreen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
VulkanVideoPresenter.HideSplashScreen();
|
||||||
|
return SetReturn(ctx, 0);
|
||||||
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, int result)
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
|||||||
@@ -3,14 +3,21 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.UserService;
|
namespace SharpEmu.Libs.UserService;
|
||||||
|
|
||||||
public static class UserServiceExports
|
public static class UserServiceExports
|
||||||
{
|
{
|
||||||
private const int OrbisUserServiceErrorInvalidArgument = unchecked((int)0x80960005);
|
private const int OrbisUserServiceErrorInvalidArgument = unchecked((int)0x80960005);
|
||||||
|
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
|
||||||
|
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
|
||||||
|
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
|
||||||
private const int PrimaryUserId = 1;
|
private const int PrimaryUserId = 1;
|
||||||
private const int InvalidUserId = -1;
|
private const int InvalidUserId = -1;
|
||||||
|
private const string PrimaryUserName = "SharpEmu";
|
||||||
|
private static int _loginEventDelivered;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "j3YMu1MVNNo",
|
Nid = "j3YMu1MVNNo",
|
||||||
@@ -64,6 +71,89 @@ public static class UserServiceExports
|
|||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "yH17Q6NWtVg",
|
||||||
|
ExportName = "sceUserServiceGetEvent",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetEvent(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var eventAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (eventAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorNoEvent);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
|
||||||
|
return ctx.Memory.TryWrite(eventAddress, payload)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1xxcMiGu2fo",
|
||||||
|
ExportName = "sceUserServiceGetUserName",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetUserName(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var capacity = ctx[CpuRegister.Rdx];
|
||||||
|
if (userId != PrimaryUserId)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nameAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
|
||||||
|
if (capacity <= (ulong)nameBytes.Length)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
||||||
|
nameBytes.CopyTo(output);
|
||||||
|
return ctx.Memory.TryWrite(nameAddress, output)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "D-CzAxQL0XI",
|
||||||
|
ExportName = "sceUserServiceGetPlatformPrivacySetting",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceUserService")]
|
||||||
|
public static int UserServiceGetPlatformPrivacySetting(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var parameterId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (parameterId != 1000)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (valueAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryWriteInt32(ctx, valueAddress, 0)
|
||||||
|
? SetReturn(ctx, 0)
|
||||||
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||||
{
|
{
|
||||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
|||||||
@@ -0,0 +1,213 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.IO.Compression;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.VideoOut;
|
||||||
|
|
||||||
|
internal static class PngSplashLoader
|
||||||
|
{
|
||||||
|
private static ReadOnlySpan<byte> PngSignature =>
|
||||||
|
[
|
||||||
|
0x89, 0x50, 0x4E, 0x47, 0x0D, 0x0A, 0x1A, 0x0A,
|
||||||
|
];
|
||||||
|
|
||||||
|
public static bool TryLoad(out byte[] pixels, out uint width, out uint height)
|
||||||
|
{
|
||||||
|
pixels = [];
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||||
|
if (string.IsNullOrWhiteSpace(app0Root))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = Path.Combine(app0Root, "sce_sys", "pic0.png");
|
||||||
|
if (!File.Exists(path))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryDecode(File.ReadAllBytes(path), out pixels, out width, out height);
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
pixels = [];
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryDecode(
|
||||||
|
ReadOnlySpan<byte> png,
|
||||||
|
out byte[] pixels,
|
||||||
|
out uint width,
|
||||||
|
out uint height)
|
||||||
|
{
|
||||||
|
pixels = [];
|
||||||
|
width = 0;
|
||||||
|
height = 0;
|
||||||
|
if (png.Length < 33 || !png[..8].SequenceEqual(PngSignature))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
byte bitDepth = 0;
|
||||||
|
byte colorType = 0;
|
||||||
|
byte interlace = 0;
|
||||||
|
using var compressed = new MemoryStream();
|
||||||
|
var offset = 8;
|
||||||
|
while (offset <= png.Length - 12)
|
||||||
|
{
|
||||||
|
var chunkLength = BinaryPrimitives.ReadUInt32BigEndian(png.Slice(offset, 4));
|
||||||
|
if (chunkLength > int.MaxValue || offset > png.Length - 12 - (int)chunkLength)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var chunkType = png.Slice(offset + 4, 4);
|
||||||
|
var chunkData = png.Slice(offset + 8, (int)chunkLength);
|
||||||
|
if (chunkType.SequenceEqual("IHDR"u8))
|
||||||
|
{
|
||||||
|
if (chunkData.Length != 13)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
width = BinaryPrimitives.ReadUInt32BigEndian(chunkData[..4]);
|
||||||
|
height = BinaryPrimitives.ReadUInt32BigEndian(chunkData.Slice(4, 4));
|
||||||
|
bitDepth = chunkData[8];
|
||||||
|
colorType = chunkData[9];
|
||||||
|
interlace = chunkData[12];
|
||||||
|
}
|
||||||
|
else if (chunkType.SequenceEqual("IDAT"u8))
|
||||||
|
{
|
||||||
|
compressed.Write(chunkData);
|
||||||
|
}
|
||||||
|
else if (chunkType.SequenceEqual("IEND"u8))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
offset += checked((int)chunkLength + 12);
|
||||||
|
}
|
||||||
|
|
||||||
|
var sourceBytesPerPixel = colorType switch
|
||||||
|
{
|
||||||
|
2 => 3,
|
||||||
|
6 => 4,
|
||||||
|
_ => 0,
|
||||||
|
};
|
||||||
|
if (width == 0 ||
|
||||||
|
height == 0 ||
|
||||||
|
width > 16384 ||
|
||||||
|
height > 16384 ||
|
||||||
|
bitDepth != 8 ||
|
||||||
|
interlace != 0 ||
|
||||||
|
sourceBytesPerPixel == 0 ||
|
||||||
|
compressed.Length == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stride = checked((int)width * sourceBytesPerPixel);
|
||||||
|
var scanlineLength = checked(stride + 1);
|
||||||
|
var decompressedLength = checked(scanlineLength * (int)height);
|
||||||
|
var scanlines = GC.AllocateUninitializedArray<byte>(decompressedLength);
|
||||||
|
compressed.Position = 0;
|
||||||
|
using (var zlib = new ZLibStream(compressed, CompressionMode.Decompress))
|
||||||
|
{
|
||||||
|
zlib.ReadExactly(scanlines);
|
||||||
|
if (zlib.ReadByte() != -1)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var reconstructed = GC.AllocateUninitializedArray<byte>(checked(stride * (int)height));
|
||||||
|
for (var y = 0; y < (int)height; y++)
|
||||||
|
{
|
||||||
|
var sourceLine = scanlines.AsSpan(y * scanlineLength + 1, stride);
|
||||||
|
var targetLine = reconstructed.AsSpan(y * stride, stride);
|
||||||
|
var previousLine = y == 0
|
||||||
|
? ReadOnlySpan<byte>.Empty
|
||||||
|
: reconstructed.AsSpan((y - 1) * stride, stride);
|
||||||
|
if (!TryUnfilter(
|
||||||
|
scanlines[y * scanlineLength],
|
||||||
|
sourceLine,
|
||||||
|
previousLine,
|
||||||
|
targetLine,
|
||||||
|
sourceBytesPerPixel))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pixels = GC.AllocateUninitializedArray<byte>(checked((int)width * (int)height * 4));
|
||||||
|
for (int sourceOffset = 0, targetOffset = 0;
|
||||||
|
sourceOffset < reconstructed.Length;
|
||||||
|
sourceOffset += sourceBytesPerPixel, targetOffset += 4)
|
||||||
|
{
|
||||||
|
pixels[targetOffset] = reconstructed[sourceOffset + 2];
|
||||||
|
pixels[targetOffset + 1] = reconstructed[sourceOffset + 1];
|
||||||
|
pixels[targetOffset + 2] = reconstructed[sourceOffset];
|
||||||
|
pixels[targetOffset + 3] = sourceBytesPerPixel == 4
|
||||||
|
? reconstructed[sourceOffset + 3]
|
||||||
|
: (byte)0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryUnfilter(
|
||||||
|
byte filter,
|
||||||
|
ReadOnlySpan<byte> source,
|
||||||
|
ReadOnlySpan<byte> previous,
|
||||||
|
Span<byte> target,
|
||||||
|
int bytesPerPixel)
|
||||||
|
{
|
||||||
|
for (var x = 0; x < source.Length; x++)
|
||||||
|
{
|
||||||
|
var left = x >= bytesPerPixel ? target[x - bytesPerPixel] : (byte)0;
|
||||||
|
var above = previous.IsEmpty ? (byte)0 : previous[x];
|
||||||
|
var upperLeft = !previous.IsEmpty && x >= bytesPerPixel
|
||||||
|
? previous[x - bytesPerPixel]
|
||||||
|
: (byte)0;
|
||||||
|
target[x] = filter switch
|
||||||
|
{
|
||||||
|
0 => source[x],
|
||||||
|
1 => unchecked((byte)(source[x] + left)),
|
||||||
|
2 => unchecked((byte)(source[x] + above)),
|
||||||
|
3 => unchecked((byte)(source[x] + ((left + above) >> 1))),
|
||||||
|
4 => unchecked((byte)(source[x] + Paeth(left, above, upperLeft))),
|
||||||
|
_ => source[x],
|
||||||
|
};
|
||||||
|
|
||||||
|
if (filter > 4)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte Paeth(byte left, byte above, byte upperLeft)
|
||||||
|
{
|
||||||
|
var estimate = left + above - upperLeft;
|
||||||
|
var leftDistance = Math.Abs(estimate - left);
|
||||||
|
var aboveDistance = Math.Abs(estimate - above);
|
||||||
|
var upperLeftDistance = Math.Abs(estimate - upperLeft);
|
||||||
|
return leftDistance <= aboveDistance && leftDistance <= upperLeftDistance
|
||||||
|
? left
|
||||||
|
: aboveDistance <= upperLeftDistance
|
||||||
|
? above
|
||||||
|
: upperLeft;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -29,8 +29,11 @@ public static class VideoOutExports
|
|||||||
private const int VideoOutBufferAttributeSize = 0x28;
|
private const int VideoOutBufferAttributeSize = 0x28;
|
||||||
private const int VideoOutBufferAttribute2Size = 0x50;
|
private const int VideoOutBufferAttribute2Size = 0x50;
|
||||||
private const int VideoOutBuffersEntrySize = 0x20;
|
private const int VideoOutBuffersEntrySize = 0x20;
|
||||||
|
private const int VideoOutOutputStatusSize = 0x30;
|
||||||
private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
|
private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
|
||||||
private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
|
private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
|
||||||
|
private const ulong SceVideoOutPixelFormatB8G8R8A8Unorm = 0x8100000000000000;
|
||||||
|
private const ulong SceVideoOutPixelFormatR8G8B8A8Unorm = 0x8100000022000000;
|
||||||
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
|
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
|
||||||
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
|
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
|
||||||
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
|
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
|
||||||
@@ -44,6 +47,36 @@ public static class VideoOutExports
|
|||||||
private static int _nextHandle = 1;
|
private static int _nextHandle = 1;
|
||||||
private static int _frameDumpCount;
|
private static int _frameDumpCount;
|
||||||
private static long _nextFrameDumpIndex;
|
private static long _nextFrameDumpIndex;
|
||||||
|
private static string _windowTitle = "SharpEmu VideoOut";
|
||||||
|
|
||||||
|
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version)
|
||||||
|
{
|
||||||
|
var parts = new List<string>();
|
||||||
|
if (!string.IsNullOrWhiteSpace(title))
|
||||||
|
{
|
||||||
|
parts.Add(title.Trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrWhiteSpace(titleId))
|
||||||
|
{
|
||||||
|
parts.Add($"[{titleId.Trim()}]");
|
||||||
|
}
|
||||||
|
|
||||||
|
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
|
||||||
|
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_windowTitle = $"SharpEmu - {application}{versionSuffix}";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static string GetWindowTitle()
|
||||||
|
{
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
return _windowTitle;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private sealed class VideoOutPortState
|
private sealed class VideoOutPortState
|
||||||
{
|
{
|
||||||
@@ -52,6 +85,10 @@ public static class VideoOutExports
|
|||||||
public ulong VblankCount { get; set; }
|
public ulong VblankCount { get; set; }
|
||||||
public ulong FlipCount { get; set; }
|
public ulong FlipCount { get; set; }
|
||||||
public int CurrentBuffer { get; set; } = -1;
|
public int CurrentBuffer { get; set; } = -1;
|
||||||
|
public uint OutputWidth { get; set; } = 1920;
|
||||||
|
public uint OutputHeight { get; set; } = 1080;
|
||||||
|
public uint RefreshRate { get; set; } = 60;
|
||||||
|
public float Gamma { get; set; } = 1.0f;
|
||||||
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
|
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
|
||||||
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
|
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
|
||||||
public List<FlipEventRegistration> FlipEvents { get; } = new();
|
public List<FlipEventRegistration> FlipEvents { get; } = new();
|
||||||
@@ -166,6 +203,93 @@ public static class VideoOutExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "utPrVdxio-8",
|
||||||
|
ExportName = "sceVideoOutGetOutputStatus",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutGetOutputStatus(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var statusAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (statusAddress == 0)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetPort(handle, out var port))
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> status = stackalloc byte[VideoOutOutputStatusSize];
|
||||||
|
status.Clear();
|
||||||
|
var resolutionClass = port.OutputWidth >= 3840 || port.OutputHeight >= 2160 ? 2 : 1;
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(status[0x00..0x04], resolutionClass);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(status[0x04..0x08], 1);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(status[0x08..0x10], port.RefreshRate);
|
||||||
|
return ctx.Memory.TryWrite(statusAddress, status)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DYhhWbJSeRg",
|
||||||
|
ExportName = "sceVideoOutColorSettingsSetGamma_",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutColorSettingsSetGamma(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var settingsAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (settingsAddress == 0)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.GetXmmRegister(0, out var xmm0Low, out _);
|
||||||
|
var gamma = BitConverter.Int32BitsToSingle(unchecked((int)xmm0Low));
|
||||||
|
if (!float.IsFinite(gamma) || gamma is < 0.1f or > 2.0f)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> gammaBytes = stackalloc byte[sizeof(float)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(gammaBytes, BitConverter.SingleToInt32Bits(gamma));
|
||||||
|
return ctx.Memory.TryWrite(settingsAddress, gammaBytes)
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "pv9CI5VC+R0",
|
||||||
|
ExportName = "sceVideoOutAdjustColor_",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutAdjustColor(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var settingsAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (settingsAddress == 0)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryGetPort(handle, out var port))
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> gammaBytes = stackalloc byte[sizeof(float)];
|
||||||
|
if (!ctx.Memory.TryRead(settingsAddress, gammaBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
port.Gamma = BitConverter.Int32BitsToSingle(
|
||||||
|
BinaryPrimitives.ReadInt32LittleEndian(gammaBytes));
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "j6RaAUlaLv0",
|
Nid = "j6RaAUlaLv0",
|
||||||
ExportName = "sceVideoOutWaitVblank",
|
ExportName = "sceVideoOutWaitVblank",
|
||||||
@@ -210,7 +334,15 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
port.FlipEvents.Add(new FlipEventRegistration(equeue, userData));
|
var existingIndex = port.FlipEvents.FindIndex(registration => registration.Equeue == equeue);
|
||||||
|
if (existingIndex >= 0)
|
||||||
|
{
|
||||||
|
port.FlipEvents[existingIndex] = new FlipEventRegistration(equeue, userData);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
port.FlipEvents.Add(new FlipEventRegistration(equeue, userData));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
TraceVideoOut($"videoout.add_flip_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||||
@@ -231,6 +363,23 @@ public static class VideoOutExports
|
|||||||
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
|
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "zgXifHT9ErY",
|
||||||
|
ExportName = "sceVideoOutIsFlipPending",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutIsFlipPending(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (!TryGetPort(handle, out _))
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "U2JJtSqNKZI",
|
Nid = "U2JJtSqNKZI",
|
||||||
ExportName = "sceVideoOutGetEventId",
|
ExportName = "sceVideoOutGetEventId",
|
||||||
@@ -284,7 +433,8 @@ public static class VideoOutExports
|
|||||||
return OrbisVideoOutErrorInvalidEvent;
|
return OrbisVideoOutErrorInvalidEvent;
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(dataAddress, data >> 16)
|
var decodedData = unchecked((ulong)(unchecked((long)data) >> 16));
|
||||||
|
return ctx.TryWriteUInt64(dataAddress, decodedData)
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -610,7 +760,7 @@ public static class VideoOutExports
|
|||||||
return OrbisVideoOutErrorInvalidIndex;
|
return OrbisVideoOutErrorInvalidIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong eventData;
|
ulong eventHint;
|
||||||
List<FlipEventRegistration> flipEvents;
|
List<FlipEventRegistration> flipEvents;
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
@@ -621,9 +771,8 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
port.CurrentBuffer = bufferIndex;
|
port.CurrentBuffer = bufferIndex;
|
||||||
port.FlipCount++;
|
port.FlipCount++;
|
||||||
var eventCount = Math.Min(port.FlipCount, 0xFUL);
|
eventHint = SceVideoOutInternalEventFlip |
|
||||||
var timeBits = (ulong)Environment.TickCount64 & 0xFFFUL;
|
((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||||
eventData = timeBits | (eventCount << 12) | ((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
|
||||||
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
|
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -637,15 +786,12 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
foreach (var flipEvent in flipEvents)
|
foreach (var flipEvent in flipEvents)
|
||||||
{
|
{
|
||||||
_ = KernelEventQueueCompatExports.EnqueueEvent(
|
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||||
flipEvent.Equeue,
|
flipEvent.Equeue,
|
||||||
new KernelEventQueueCompatExports.KernelQueuedEvent(
|
SceVideoOutInternalEventFlip,
|
||||||
SceVideoOutInternalEventFlip,
|
OrbisKernelEventFilterVideoOut,
|
||||||
OrbisKernelEventFilterVideoOut,
|
eventHint,
|
||||||
0,
|
flipEvent.UserData);
|
||||||
0,
|
|
||||||
eventData,
|
|
||||||
flipEvent.UserData));
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
||||||
@@ -680,6 +826,8 @@ public static class VideoOutExports
|
|||||||
Index = groupIndex,
|
Index = groupIndex,
|
||||||
Attribute = attribute,
|
Attribute = attribute,
|
||||||
};
|
};
|
||||||
|
port.OutputWidth = attribute.Width;
|
||||||
|
port.OutputHeight = attribute.Height;
|
||||||
|
|
||||||
for (var i = 0; i < addresses.Length; i++)
|
for (var i = 0; i < addresses.Length; i++)
|
||||||
{
|
{
|
||||||
@@ -691,6 +839,7 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
TraceVideoOut(
|
TraceVideoOut(
|
||||||
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||||
|
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
|
||||||
return groupIndex;
|
return groupIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -904,6 +1053,8 @@ public static class VideoOutExports
|
|||||||
private static uint GetBytesPerPixel(ulong pixelFormat) =>
|
private static uint GetBytesPerPixel(ulong pixelFormat) =>
|
||||||
pixelFormat is SceVideoOutPixelFormatA8R8G8B8Srgb or
|
pixelFormat is SceVideoOutPixelFormatA8R8G8B8Srgb or
|
||||||
SceVideoOutPixelFormatA8B8G8R8Srgb or
|
SceVideoOutPixelFormatA8B8G8R8Srgb or
|
||||||
|
SceVideoOutPixelFormatB8G8R8A8Unorm or
|
||||||
|
SceVideoOutPixelFormatR8G8B8A8Unorm or
|
||||||
SceVideoOutPixelFormatA2R10G10B10 or
|
SceVideoOutPixelFormatA2R10G10B10 or
|
||||||
SceVideoOutPixelFormatA2R10G10B10Srgb or
|
SceVideoOutPixelFormatA2R10G10B10Srgb or
|
||||||
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq
|
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq
|
||||||
@@ -938,7 +1089,7 @@ public static class VideoOutExports
|
|||||||
var dst = 0;
|
var dst = 0;
|
||||||
for (var src = 0; src + 3 < source.Length; src += 4)
|
for (var src = 0; src + 3 < source.Length; src += 4)
|
||||||
{
|
{
|
||||||
if (pixelFormat == SceVideoOutPixelFormatA8B8G8R8Srgb)
|
if (pixelFormat is SceVideoOutPixelFormatA8B8G8R8Srgb or SceVideoOutPixelFormatR8G8B8A8Unorm)
|
||||||
{
|
{
|
||||||
destination[dst++] = source[src + 0];
|
destination[dst++] = source[src + 0];
|
||||||
destination[dst++] = source[src + 1];
|
destination[dst++] = source[src + 1];
|
||||||
|
|||||||
@@ -12,15 +12,103 @@ using VkSemaphore = Silk.NET.Vulkan.Semaphore;
|
|||||||
|
|
||||||
namespace SharpEmu.Libs.VideoOut;
|
namespace SharpEmu.Libs.VideoOut;
|
||||||
|
|
||||||
|
internal enum GuestDrawKind
|
||||||
|
{
|
||||||
|
None,
|
||||||
|
FullscreenBarycentric,
|
||||||
|
}
|
||||||
|
|
||||||
internal static unsafe class VulkanVideoPresenter
|
internal static unsafe class VulkanVideoPresenter
|
||||||
{
|
{
|
||||||
private static readonly object _gate = new();
|
private static readonly object _gate = new();
|
||||||
private static Thread? _thread;
|
private static Thread? _thread;
|
||||||
private static byte[]? _latestFrame;
|
private static Presentation? _latestPresentation;
|
||||||
private static uint _latestWidth;
|
private static uint _windowWidth;
|
||||||
private static uint _latestHeight;
|
private static uint _windowHeight;
|
||||||
private static long _latestSequence;
|
|
||||||
private static bool _closed;
|
private static bool _closed;
|
||||||
|
private static bool _splashHidden;
|
||||||
|
|
||||||
|
public static void EnsureStarted(uint width, uint height)
|
||||||
|
{
|
||||||
|
if (width == 0 || height == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_closed || _thread is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasSplash = PngSplashLoader.TryLoad(
|
||||||
|
out var splashPixels,
|
||||||
|
out var splashWidth,
|
||||||
|
out var splashHeight);
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_closed || _thread is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_windowWidth = width;
|
||||||
|
_windowHeight = height;
|
||||||
|
_latestPresentation ??= _splashHidden
|
||||||
|
? new Presentation(
|
||||||
|
CreateBlackFrame(width, height),
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
1,
|
||||||
|
GuestDrawKind.None,
|
||||||
|
IsSplash: false)
|
||||||
|
: hasSplash
|
||||||
|
? new Presentation(
|
||||||
|
splashPixels,
|
||||||
|
splashWidth,
|
||||||
|
splashHeight,
|
||||||
|
1,
|
||||||
|
GuestDrawKind.None,
|
||||||
|
IsSplash: true)
|
||||||
|
: new Presentation(
|
||||||
|
null,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
0,
|
||||||
|
GuestDrawKind.None,
|
||||||
|
IsSplash: false);
|
||||||
|
_thread = new Thread(Run)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "SharpEmu Vulkan VideoOut",
|
||||||
|
};
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void HideSplashScreen()
|
||||||
|
{
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_splashHidden = true;
|
||||||
|
if (_closed || _latestPresentation is not { IsSplash: true } latest)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sequence = latest.Sequence + 1;
|
||||||
|
_latestPresentation = new Presentation(
|
||||||
|
CreateBlackFrame(latest.Width, latest.Height),
|
||||||
|
latest.Width,
|
||||||
|
latest.Height,
|
||||||
|
sequence,
|
||||||
|
GuestDrawKind.None,
|
||||||
|
IsSplash: false);
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] Vulkan VideoOut hid splash");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
public static void Submit(byte[] bgraFrame, uint width, uint height)
|
public static void Submit(byte[] bgraFrame, uint width, uint height)
|
||||||
{
|
{
|
||||||
@@ -36,15 +124,21 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_latestFrame = bgraFrame;
|
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
|
||||||
_latestWidth = width;
|
_latestPresentation = new Presentation(
|
||||||
_latestHeight = height;
|
bgraFrame,
|
||||||
_latestSequence++;
|
width,
|
||||||
|
height,
|
||||||
|
sequence,
|
||||||
|
GuestDrawKind.None,
|
||||||
|
IsSplash: false);
|
||||||
if (_thread is not null)
|
if (_thread is not null)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_windowWidth = width;
|
||||||
|
_windowHeight = height;
|
||||||
_thread = new Thread(Run)
|
_thread = new Thread(Run)
|
||||||
{
|
{
|
||||||
IsBackground = true,
|
IsBackground = true,
|
||||||
@@ -54,14 +148,74 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void SubmitGuestDraw(GuestDrawKind drawKind, uint width, uint height)
|
||||||
|
{
|
||||||
|
if (drawKind == GuestDrawKind.None || width == 0 || height == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
if (_closed ||
|
||||||
|
_latestPresentation is { Pixels: null } latest &&
|
||||||
|
latest.DrawKind == drawKind &&
|
||||||
|
latest.Width == width &&
|
||||||
|
latest.Height == height)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sequence = (_latestPresentation?.Sequence ?? 0) + 1;
|
||||||
|
_latestPresentation = new Presentation(
|
||||||
|
null,
|
||||||
|
width,
|
||||||
|
height,
|
||||||
|
sequence,
|
||||||
|
drawKind,
|
||||||
|
IsSplash: false);
|
||||||
|
if (_thread is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_windowWidth = width;
|
||||||
|
_windowHeight = height;
|
||||||
|
_thread = new Thread(Run)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "SharpEmu Vulkan VideoOut",
|
||||||
|
};
|
||||||
|
_thread.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte[] CreateBlackFrame(uint width, uint height)
|
||||||
|
{
|
||||||
|
if (width == 0 || height == 0 || width > 8192 || height > 8192)
|
||||||
|
{
|
||||||
|
width = 1;
|
||||||
|
height = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var pixels = GC.AllocateUninitializedArray<byte>(checked((int)(width * height * 4)));
|
||||||
|
pixels.AsSpan().Clear();
|
||||||
|
for (var offset = 3; offset < pixels.Length; offset += 4)
|
||||||
|
{
|
||||||
|
pixels[offset] = 0xFF;
|
||||||
|
}
|
||||||
|
|
||||||
|
return pixels;
|
||||||
|
}
|
||||||
|
|
||||||
private static void Run()
|
private static void Run()
|
||||||
{
|
{
|
||||||
uint width;
|
uint width;
|
||||||
uint height;
|
uint height;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
width = _latestWidth;
|
width = _windowWidth == 0 ? _latestPresentation?.Width ?? 1280 : _windowWidth;
|
||||||
height = _latestHeight;
|
height = _windowHeight == 0 ? _latestPresentation?.Height ?? 720 : _windowHeight;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -83,25 +237,37 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryTakeFrame(long presentedSequence, out Frame frame)
|
private static bool TryTakePresentation(long presentedSequence, out Presentation presentation)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (_latestFrame is null || _latestSequence == presentedSequence)
|
if (_latestPresentation is not { } latest || latest.Sequence == presentedSequence)
|
||||||
{
|
{
|
||||||
frame = default;
|
presentation = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
frame = new Frame(_latestFrame, _latestWidth, _latestHeight, _latestSequence);
|
presentation = latest;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct Frame(byte[] Pixels, uint Width, uint Height, long Sequence);
|
private readonly record struct Presentation(
|
||||||
|
byte[]? Pixels,
|
||||||
|
uint Width,
|
||||||
|
uint Height,
|
||||||
|
long Sequence,
|
||||||
|
GuestDrawKind DrawKind,
|
||||||
|
bool IsSplash);
|
||||||
|
|
||||||
private sealed class Presenter : IDisposable
|
private sealed class Presenter : IDisposable
|
||||||
{
|
{
|
||||||
|
private const string FullscreenBarycentricVertexSpirv =
|
||||||
|
"AwIjBwAAAQALAAgAMgAAAAAAAAARAAIAAQAAAAsABgABAAAAR0xTTC5zdGQuNDUwAAAAAA4AAwAAAAAAAQAAAA8ACAAAAAAABAAAAG1haW4AAAAADQAAABoAAAApAAAAAwADAAIAAADCAQAABQAEAAQAAABtYWluAAAAAAUABgALAAAAZ2xfUGVyVmVydGV4AAAAAAYABgALAAAAAAAAAGdsX1Bvc2l0aW9uAAYABwALAAAAAQAAAGdsX1BvaW50U2l6ZQAAAAAGAAcACwAAAAIAAABnbF9DbGlwRGlzdGFuY2UABgAHAAsAAAADAAAAZ2xfQ3VsbERpc3RhbmNlAAUAAwANAAAAAAAAAAUABgAaAAAAZ2xfVmVydGV4SW5kZXgAAAUABQAdAAAAaW5kZXhhYmxlAAAABQAFACkAAABiYXJ5Y2VudHJpYwAFAAUALwAAAGluZGV4YWJsZQAAAEcAAwALAAAAAgAAAEgABQALAAAAAAAAAAsAAAAAAAAASAAFAAsAAAABAAAACwAAAAEAAABIAAUACwAAAAIAAAALAAAAAwAAAEgABQALAAAAAwAAAAsAAAAEAAAARwAEABoAAAALAAAAKgAAAEcABAApAAAAHgAAAAAAAAATAAIAAgAAACEAAwADAAAAAgAAABYAAwAGAAAAIAAAABcABAAHAAAABgAAAAQAAAAVAAQACAAAACAAAAAAAAAAKwAEAAgAAAAJAAAAAQAAABwABAAKAAAABgAAAAkAAAAeAAYACwAAAAcAAAAGAAAACgAAAAoAAAAgAAQADAAAAAMAAAALAAAAOwAEAAwAAAANAAAAAwAAABUABAAOAAAAIAAAAAEAAAArAAQADgAAAA8AAAAAAAAAFwAEABAAAAAGAAAAAgAAACsABAAIAAAAEQAAAAMAAAAcAAQAEgAAABAAAAARAAAAKwAEAAYAAAATAAAAAACAvywABQAQAAAAFAAAABMAAAATAAAAKwAEAAYAAAAVAAAAAABAQCwABQAQAAAAFgAAABUAAAATAAAALAAFABAAAAAXAAAAEwAAABUAAAAsAAYAEgAAABgAAAAUAAAAFgAAABcAAAAgAAQAGQAAAAEAAAAOAAAAOwAEABkAAAAaAAAAAQAAACAABAAcAAAABwAAABIAAAAgAAQAHgAAAAcAAAAQAAAAKwAEAAYAAAAhAAAAAAAAACsABAAGAAAAIgAAAAAAgD8gAAQAJgAAAAMAAAAHAAAAIAAEACgAAAADAAAAEAAAADsABAAoAAAAKQAAAAMAAAAsAAUAEAAAACoAAAAiAAAAIQAAACwABQAQAAAAKwAAACEAAAAiAAAALAAFABAAAAAsAAAAIQAAACEAAAAsAAYAEgAAAC0AAAAqAAAAKwAAACwAAAA2AAUAAgAAAAQAAAAAAAAAAwAAAPgAAgAFAAAAOwAEABwAAAAdAAAABwAAADsABAAcAAAALwAAAAcAAAA9AAQADgAAABsAAAAaAAAAPgADAB0AAAAYAAAAQQAFAB4AAAAfAAAAHQAAABsAAAA9AAQAEAAAACAAAAAfAAAAUQAFAAYAAAAjAAAAIAAAAAAAAABRAAUABgAAACQAAAAgAAAAAQAAAFAABwAHAAAAJQAAACMAAAAkAAAAIQAAACIAAABBAAUAJgAAACcAAAANAAAADwAAAD4AAwAnAAAAJQAAAD0ABAAOAAAALgAAABoAAAA+AAMALwAAAC0AAABBAAUAHgAAADAAAAAvAAAALgAAAD0ABAAQAAAAMQAAADAAAAA+AAMAKQAAADEAAAD9AAEAOAABAA==";
|
||||||
|
|
||||||
|
private const string FullscreenBarycentricFragmentSpirv =
|
||||||
|
"AwIjBwAAAQALAAgAEgAAAAAAAAARAAIAAQAAAAsABgABAAAAR0xTTC5zdGQuNDUwAAAAAA4AAwAAAAAAAQAAAA8ABwAEAAAABAAAAG1haW4AAAAACQAAAAwAAAAQAAMABAAAAAcAAAADAAMAAgAAAMIBAAAFAAQABAAAAG1haW4AAAAABQAFAAkAAABvdXRDb2xvcgAAAAAFAAUADAAAAGJhcnljZW50cmljAEcABAAJAAAAHgAAAAAAAABHAAQADAAAAB4AAAAAAAAAEwACAAIAAAAhAAMAAwAAAAIAAAAWAAMABgAAACAAAAAXAAQABwAAAAYAAAAEAAAAIAAEAAgAAAADAAAABwAAADsABAAIAAAACQAAAAMAAAAXAAQACgAAAAYAAAACAAAAIAAEAAsAAAABAAAACgAAADsABAALAAAADAAAAAEAAAArAAQABgAAAA4AAAAAAAAANgAFAAIAAAAEAAAAAAAAAAMAAAD4AAIABQAAAD0ABAAKAAAADQAAAAwAAABRAAUABgAAAA8AAAANAAAAAAAAAFEABQAGAAAAEAAAAA0AAAABAAAAUAAHAAcAAAARAAAADwAAABAAAAAOAAAADgAAAD4AAwAJAAAAEQAAAP0AAQA4AAEA";
|
||||||
|
|
||||||
private readonly IWindow _window;
|
private readonly IWindow _window;
|
||||||
private Vk _vk = null!;
|
private Vk _vk = null!;
|
||||||
private KhrSurface _surfaceApi = null!;
|
private KhrSurface _surfaceApi = null!;
|
||||||
@@ -114,9 +280,14 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private uint _queueFamilyIndex;
|
private uint _queueFamilyIndex;
|
||||||
private SwapchainKHR _swapchain;
|
private SwapchainKHR _swapchain;
|
||||||
private Image[] _swapchainImages = [];
|
private Image[] _swapchainImages = [];
|
||||||
|
private ImageView[] _swapchainImageViews = [];
|
||||||
|
private Framebuffer[] _framebuffers = [];
|
||||||
private bool[] _imageInitialized = [];
|
private bool[] _imageInitialized = [];
|
||||||
private Format _swapchainFormat;
|
private Format _swapchainFormat;
|
||||||
private Extent2D _extent;
|
private Extent2D _extent;
|
||||||
|
private RenderPass _renderPass;
|
||||||
|
private PipelineLayout _pipelineLayout;
|
||||||
|
private Pipeline _barycentricPipeline;
|
||||||
private CommandPool _commandPool;
|
private CommandPool _commandPool;
|
||||||
private CommandBuffer _commandBuffer;
|
private CommandBuffer _commandBuffer;
|
||||||
private VkSemaphore _imageAvailable;
|
private VkSemaphore _imageAvailable;
|
||||||
@@ -127,14 +298,18 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private long _presentedSequence;
|
private long _presentedSequence;
|
||||||
private bool _vulkanReady;
|
private bool _vulkanReady;
|
||||||
private bool _firstFramePresented;
|
private bool _firstFramePresented;
|
||||||
|
private bool _firstGuestDrawPresented;
|
||||||
|
private bool _splashPresented;
|
||||||
|
|
||||||
public Presenter(uint width, uint height)
|
public Presenter(uint width, uint height)
|
||||||
{
|
{
|
||||||
var options = WindowOptions.DefaultVulkan;
|
var options = WindowOptions.DefaultVulkan;
|
||||||
options.Size = new Vector2D<int>((int)width, (int)height);
|
options.Size = new Vector2D<int>((int)width, (int)height);
|
||||||
options.Title = "SharpEmu VideoOut";
|
options.Title = VideoOutExports.GetWindowTitle();
|
||||||
options.WindowBorder = WindowBorder.Fixed;
|
options.WindowBorder = WindowBorder.Fixed;
|
||||||
options.VSync = true;
|
options.VSync = true;
|
||||||
|
options.FramesPerSecond = 60;
|
||||||
|
options.UpdatesPerSecond = 60;
|
||||||
_window = Window.Create(options);
|
_window = Window.Create(options);
|
||||||
_window.Load += Initialize;
|
_window.Load += Initialize;
|
||||||
_window.Render += Render;
|
_window.Render += Render;
|
||||||
@@ -158,6 +333,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
CreateDevice();
|
CreateDevice();
|
||||||
CreateSwapchain();
|
CreateSwapchain();
|
||||||
CreateCommandResources();
|
CreateCommandResources();
|
||||||
|
CreateGuestDrawResources();
|
||||||
_vulkanReady = true;
|
_vulkanReady = true;
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][INFO] Vulkan VideoOut ready: {_extent.Width}x{_extent.Height}, format={_swapchainFormat}");
|
$"[LOADER][INFO] Vulkan VideoOut ready: {_extent.Width}x{_extent.Height}, format={_swapchainFormat}");
|
||||||
@@ -323,7 +499,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
ImageColorSpace = surfaceFormat.ColorSpace,
|
ImageColorSpace = surfaceFormat.ColorSpace,
|
||||||
ImageExtent = _extent,
|
ImageExtent = _extent,
|
||||||
ImageArrayLayers = 1,
|
ImageArrayLayers = 1,
|
||||||
ImageUsage = ImageUsageFlags.TransferDstBit,
|
ImageUsage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.ColorAttachmentBit,
|
||||||
ImageSharingMode = SharingMode.Exclusive,
|
ImageSharingMode = SharingMode.Exclusive,
|
||||||
PreTransform = capabilities.CurrentTransform,
|
PreTransform = capabilities.CurrentTransform,
|
||||||
CompositeAlpha = compositeAlpha,
|
CompositeAlpha = compositeAlpha,
|
||||||
@@ -377,6 +553,218 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4);
|
CreateStagingBuffer((ulong)_extent.Width * _extent.Height * 4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void CreateGuestDrawResources()
|
||||||
|
{
|
||||||
|
var colorAttachment = new AttachmentDescription
|
||||||
|
{
|
||||||
|
Format = _swapchainFormat,
|
||||||
|
Samples = SampleCountFlags.Count1Bit,
|
||||||
|
LoadOp = AttachmentLoadOp.Clear,
|
||||||
|
StoreOp = AttachmentStoreOp.Store,
|
||||||
|
StencilLoadOp = AttachmentLoadOp.DontCare,
|
||||||
|
StencilStoreOp = AttachmentStoreOp.DontCare,
|
||||||
|
InitialLayout = ImageLayout.Undefined,
|
||||||
|
FinalLayout = ImageLayout.PresentSrcKhr,
|
||||||
|
};
|
||||||
|
var colorReference = new AttachmentReference
|
||||||
|
{
|
||||||
|
Attachment = 0,
|
||||||
|
Layout = ImageLayout.ColorAttachmentOptimal,
|
||||||
|
};
|
||||||
|
var subpass = new SubpassDescription
|
||||||
|
{
|
||||||
|
PipelineBindPoint = PipelineBindPoint.Graphics,
|
||||||
|
ColorAttachmentCount = 1,
|
||||||
|
PColorAttachments = &colorReference,
|
||||||
|
};
|
||||||
|
var dependency = new SubpassDependency
|
||||||
|
{
|
||||||
|
SrcSubpass = Vk.SubpassExternal,
|
||||||
|
DstSubpass = 0,
|
||||||
|
SrcStageMask = PipelineStageFlags.ColorAttachmentOutputBit,
|
||||||
|
DstStageMask = PipelineStageFlags.ColorAttachmentOutputBit,
|
||||||
|
DstAccessMask = AccessFlags.ColorAttachmentWriteBit,
|
||||||
|
};
|
||||||
|
var renderPassInfo = new RenderPassCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.RenderPassCreateInfo,
|
||||||
|
AttachmentCount = 1,
|
||||||
|
PAttachments = &colorAttachment,
|
||||||
|
SubpassCount = 1,
|
||||||
|
PSubpasses = &subpass,
|
||||||
|
DependencyCount = 1,
|
||||||
|
PDependencies = &dependency,
|
||||||
|
};
|
||||||
|
Check(_vk.CreateRenderPass(_device, &renderPassInfo, null, out _renderPass), "vkCreateRenderPass");
|
||||||
|
|
||||||
|
_swapchainImageViews = new ImageView[_swapchainImages.Length];
|
||||||
|
_framebuffers = new Framebuffer[_swapchainImages.Length];
|
||||||
|
for (var index = 0; index < _swapchainImages.Length; index++)
|
||||||
|
{
|
||||||
|
var viewInfo = new ImageViewCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.ImageViewCreateInfo,
|
||||||
|
Image = _swapchainImages[index],
|
||||||
|
ViewType = ImageViewType.Type2D,
|
||||||
|
Format = _swapchainFormat,
|
||||||
|
Components = new ComponentMapping(
|
||||||
|
ComponentSwizzle.Identity,
|
||||||
|
ComponentSwizzle.Identity,
|
||||||
|
ComponentSwizzle.Identity,
|
||||||
|
ComponentSwizzle.Identity),
|
||||||
|
SubresourceRange = ColorSubresourceRange(),
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
_vk.CreateImageView(_device, &viewInfo, null, out _swapchainImageViews[index]),
|
||||||
|
"vkCreateImageView");
|
||||||
|
|
||||||
|
var imageView = _swapchainImageViews[index];
|
||||||
|
var framebufferInfo = new FramebufferCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.FramebufferCreateInfo,
|
||||||
|
RenderPass = _renderPass,
|
||||||
|
AttachmentCount = 1,
|
||||||
|
PAttachments = &imageView,
|
||||||
|
Width = _extent.Width,
|
||||||
|
Height = _extent.Height,
|
||||||
|
Layers = 1,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
_vk.CreateFramebuffer(_device, &framebufferInfo, null, out _framebuffers[index]),
|
||||||
|
"vkCreateFramebuffer");
|
||||||
|
}
|
||||||
|
|
||||||
|
var layoutInfo = new PipelineLayoutCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineLayoutCreateInfo,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
_vk.CreatePipelineLayout(_device, &layoutInfo, null, out _pipelineLayout),
|
||||||
|
"vkCreatePipelineLayout");
|
||||||
|
CreateBarycentricPipeline();
|
||||||
|
}
|
||||||
|
|
||||||
|
private void CreateBarycentricPipeline()
|
||||||
|
{
|
||||||
|
var vertexBytes = Convert.FromBase64String(FullscreenBarycentricVertexSpirv);
|
||||||
|
var fragmentBytes = Convert.FromBase64String(FullscreenBarycentricFragmentSpirv);
|
||||||
|
var vertexModule = CreateShaderModule(vertexBytes);
|
||||||
|
var fragmentModule = CreateShaderModule(fragmentBytes);
|
||||||
|
var entryPoint = (byte*)SilkMarshal.StringToPtr("main");
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var shaderStages = stackalloc PipelineShaderStageCreateInfo[2];
|
||||||
|
shaderStages[0] = new PipelineShaderStageCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineShaderStageCreateInfo,
|
||||||
|
Stage = ShaderStageFlags.VertexBit,
|
||||||
|
Module = vertexModule,
|
||||||
|
PName = entryPoint,
|
||||||
|
};
|
||||||
|
shaderStages[1] = new PipelineShaderStageCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineShaderStageCreateInfo,
|
||||||
|
Stage = ShaderStageFlags.FragmentBit,
|
||||||
|
Module = fragmentModule,
|
||||||
|
PName = entryPoint,
|
||||||
|
};
|
||||||
|
|
||||||
|
var vertexInput = new PipelineVertexInputStateCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineVertexInputStateCreateInfo,
|
||||||
|
};
|
||||||
|
var inputAssembly = new PipelineInputAssemblyStateCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineInputAssemblyStateCreateInfo,
|
||||||
|
Topology = PrimitiveTopology.TriangleList,
|
||||||
|
};
|
||||||
|
var viewport = new Viewport(0, 0, _extent.Width, _extent.Height, 0, 1);
|
||||||
|
var scissor = new Rect2D(new Offset2D(0, 0), _extent);
|
||||||
|
var viewportState = new PipelineViewportStateCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineViewportStateCreateInfo,
|
||||||
|
ViewportCount = 1,
|
||||||
|
PViewports = &viewport,
|
||||||
|
ScissorCount = 1,
|
||||||
|
PScissors = &scissor,
|
||||||
|
};
|
||||||
|
var rasterization = new PipelineRasterizationStateCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineRasterizationStateCreateInfo,
|
||||||
|
PolygonMode = PolygonMode.Fill,
|
||||||
|
CullMode = CullModeFlags.None,
|
||||||
|
FrontFace = FrontFace.CounterClockwise,
|
||||||
|
LineWidth = 1,
|
||||||
|
};
|
||||||
|
var multisample = new PipelineMultisampleStateCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineMultisampleStateCreateInfo,
|
||||||
|
RasterizationSamples = SampleCountFlags.Count1Bit,
|
||||||
|
};
|
||||||
|
var colorBlendAttachment = new PipelineColorBlendAttachmentState
|
||||||
|
{
|
||||||
|
ColorWriteMask =
|
||||||
|
ColorComponentFlags.RBit |
|
||||||
|
ColorComponentFlags.GBit |
|
||||||
|
ColorComponentFlags.BBit |
|
||||||
|
ColorComponentFlags.ABit,
|
||||||
|
};
|
||||||
|
var colorBlend = new PipelineColorBlendStateCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.PipelineColorBlendStateCreateInfo,
|
||||||
|
AttachmentCount = 1,
|
||||||
|
PAttachments = &colorBlendAttachment,
|
||||||
|
};
|
||||||
|
var pipelineInfo = new GraphicsPipelineCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.GraphicsPipelineCreateInfo,
|
||||||
|
StageCount = 2,
|
||||||
|
PStages = shaderStages,
|
||||||
|
PVertexInputState = &vertexInput,
|
||||||
|
PInputAssemblyState = &inputAssembly,
|
||||||
|
PViewportState = &viewportState,
|
||||||
|
PRasterizationState = &rasterization,
|
||||||
|
PMultisampleState = &multisample,
|
||||||
|
PColorBlendState = &colorBlend,
|
||||||
|
Layout = _pipelineLayout,
|
||||||
|
RenderPass = _renderPass,
|
||||||
|
Subpass = 0,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
_vk.CreateGraphicsPipelines(
|
||||||
|
_device,
|
||||||
|
default,
|
||||||
|
1,
|
||||||
|
&pipelineInfo,
|
||||||
|
null,
|
||||||
|
out _barycentricPipeline),
|
||||||
|
"vkCreateGraphicsPipelines");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
SilkMarshal.Free((nint)entryPoint);
|
||||||
|
_vk.DestroyShaderModule(_device, fragmentModule, null);
|
||||||
|
_vk.DestroyShaderModule(_device, vertexModule, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private ShaderModule CreateShaderModule(byte[] code)
|
||||||
|
{
|
||||||
|
fixed (byte* codePointer = code)
|
||||||
|
{
|
||||||
|
var createInfo = new ShaderModuleCreateInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.ShaderModuleCreateInfo,
|
||||||
|
CodeSize = (nuint)code.Length,
|
||||||
|
PCode = (uint*)codePointer,
|
||||||
|
};
|
||||||
|
Check(
|
||||||
|
_vk.CreateShaderModule(_device, &createInfo, null, out var module),
|
||||||
|
"vkCreateShaderModule");
|
||||||
|
return module;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void CreateStagingBuffer(ulong size)
|
private void CreateStagingBuffer(ulong size)
|
||||||
{
|
{
|
||||||
var bufferInfo = new BufferCreateInfo
|
var bufferInfo = new BufferCreateInfo
|
||||||
@@ -420,19 +808,34 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private void Render(double _)
|
private void Render(double _)
|
||||||
{
|
{
|
||||||
if (!_vulkanReady || !TryTakeFrame(_presentedSequence, out var frame))
|
if (!_vulkanReady || !TryTakePresentation(_presentedSequence, out var presentation))
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var pixels = frame.Width == _extent.Width && frame.Height == _extent.Height
|
if (presentation.Pixels is null &&
|
||||||
? frame.Pixels
|
presentation.DrawKind != GuestDrawKind.FullscreenBarycentric)
|
||||||
: ScaleBgra(frame.Pixels, frame.Width, frame.Height, _extent.Width, _extent.Height);
|
|
||||||
if ((ulong)pixels.Length > _stagingSize)
|
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
byte[]? pixels = null;
|
||||||
|
if (presentation.Pixels is { } sourcePixels)
|
||||||
|
{
|
||||||
|
pixels = presentation.Width == _extent.Width && presentation.Height == _extent.Height
|
||||||
|
? sourcePixels
|
||||||
|
: ScaleBgra(
|
||||||
|
sourcePixels,
|
||||||
|
presentation.Width,
|
||||||
|
presentation.Height,
|
||||||
|
_extent.Width,
|
||||||
|
_extent.Height);
|
||||||
|
if ((ulong)pixels.Length > _stagingSize)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
uint imageIndex;
|
uint imageIndex;
|
||||||
Check(
|
Check(
|
||||||
_swapchainApi.AcquireNextImage(
|
_swapchainApi.AcquireNextImage(
|
||||||
@@ -444,15 +847,18 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
&imageIndex),
|
&imageIndex),
|
||||||
"vkAcquireNextImageKHR");
|
"vkAcquireNextImageKHR");
|
||||||
|
|
||||||
void* mapped;
|
if (pixels is not null)
|
||||||
Check(
|
|
||||||
_vk.MapMemory(_device, _stagingMemory, 0, (ulong)pixels.Length, 0, &mapped),
|
|
||||||
"vkMapMemory");
|
|
||||||
fixed (byte* source = pixels)
|
|
||||||
{
|
{
|
||||||
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
|
void* mapped;
|
||||||
|
Check(
|
||||||
|
_vk.MapMemory(_device, _stagingMemory, 0, (ulong)pixels.Length, 0, &mapped),
|
||||||
|
"vkMapMemory");
|
||||||
|
fixed (byte* source = pixels)
|
||||||
|
{
|
||||||
|
System.Buffer.MemoryCopy(source, mapped, pixels.Length, pixels.Length);
|
||||||
|
}
|
||||||
|
_vk.UnmapMemory(_device, _stagingMemory);
|
||||||
}
|
}
|
||||||
_vk.UnmapMemory(_device, _stagingMemory);
|
|
||||||
|
|
||||||
Check(_vk.ResetCommandBuffer(_commandBuffer, 0), "vkResetCommandBuffer");
|
Check(_vk.ResetCommandBuffer(_commandBuffer, 0), "vkResetCommandBuffer");
|
||||||
var beginInfo = new CommandBufferBeginInfo
|
var beginInfo = new CommandBufferBeginInfo
|
||||||
@@ -462,6 +868,100 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
};
|
};
|
||||||
Check(_vk.BeginCommandBuffer(_commandBuffer, &beginInfo), "vkBeginCommandBuffer");
|
Check(_vk.BeginCommandBuffer(_commandBuffer, &beginInfo), "vkBeginCommandBuffer");
|
||||||
|
|
||||||
|
PipelineStageFlags waitStage;
|
||||||
|
if (pixels is not null)
|
||||||
|
{
|
||||||
|
RecordUpload(imageIndex);
|
||||||
|
waitStage = PipelineStageFlags.TransferBit;
|
||||||
|
}
|
||||||
|
else if (presentation.DrawKind == GuestDrawKind.FullscreenBarycentric)
|
||||||
|
{
|
||||||
|
var clearValue = default(ClearValue);
|
||||||
|
var renderPassInfo = new RenderPassBeginInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.RenderPassBeginInfo,
|
||||||
|
RenderPass = _renderPass,
|
||||||
|
Framebuffer = _framebuffers[imageIndex],
|
||||||
|
RenderArea = new Rect2D(new Offset2D(0, 0), _extent),
|
||||||
|
ClearValueCount = 1,
|
||||||
|
PClearValues = &clearValue,
|
||||||
|
};
|
||||||
|
_vk.CmdBeginRenderPass(
|
||||||
|
_commandBuffer,
|
||||||
|
&renderPassInfo,
|
||||||
|
SubpassContents.Inline);
|
||||||
|
_vk.CmdBindPipeline(
|
||||||
|
_commandBuffer,
|
||||||
|
PipelineBindPoint.Graphics,
|
||||||
|
_barycentricPipeline);
|
||||||
|
_vk.CmdDraw(_commandBuffer, 3, 1, 0, 0);
|
||||||
|
_vk.CmdEndRenderPass(_commandBuffer);
|
||||||
|
waitStage = PipelineStageFlags.ColorAttachmentOutputBit;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException(
|
||||||
|
$"Unsupported translated guest draw: {presentation.DrawKind}.");
|
||||||
|
}
|
||||||
|
|
||||||
|
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer");
|
||||||
|
|
||||||
|
var imageAvailable = _imageAvailable;
|
||||||
|
var commandBuffer = _commandBuffer;
|
||||||
|
var renderFinished = _renderFinished;
|
||||||
|
var submitInfo = new SubmitInfo
|
||||||
|
{
|
||||||
|
SType = StructureType.SubmitInfo,
|
||||||
|
WaitSemaphoreCount = 1,
|
||||||
|
PWaitSemaphores = &imageAvailable,
|
||||||
|
PWaitDstStageMask = &waitStage,
|
||||||
|
CommandBufferCount = 1,
|
||||||
|
PCommandBuffers = &commandBuffer,
|
||||||
|
SignalSemaphoreCount = 1,
|
||||||
|
PSignalSemaphores = &renderFinished,
|
||||||
|
};
|
||||||
|
Check(_vk.QueueSubmit(_queue, 1, &submitInfo, default), "vkQueueSubmit");
|
||||||
|
|
||||||
|
var swapchain = _swapchain;
|
||||||
|
var presentInfo = new PresentInfoKHR
|
||||||
|
{
|
||||||
|
SType = StructureType.PresentInfoKhr,
|
||||||
|
WaitSemaphoreCount = 1,
|
||||||
|
PWaitSemaphores = &renderFinished,
|
||||||
|
SwapchainCount = 1,
|
||||||
|
PSwapchains = &swapchain,
|
||||||
|
PImageIndices = &imageIndex,
|
||||||
|
};
|
||||||
|
Check(_swapchainApi.QueuePresent(_queue, &presentInfo), "vkQueuePresentKHR");
|
||||||
|
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
|
||||||
|
_imageInitialized[imageIndex] = true;
|
||||||
|
_presentedSequence = presentation.Sequence;
|
||||||
|
if (presentation.IsSplash && !_splashPresented)
|
||||||
|
{
|
||||||
|
_splashPresented = true;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Vulkan VideoOut presented splash: " +
|
||||||
|
$"{presentation.Width}x{presentation.Height}");
|
||||||
|
}
|
||||||
|
else if (!presentation.IsSplash && !_firstFramePresented)
|
||||||
|
{
|
||||||
|
_firstFramePresented = true;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Vulkan VideoOut presented first frame: " +
|
||||||
|
$"{presentation.Width}x{presentation.Height}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pixels is null && !_firstGuestDrawPresented)
|
||||||
|
{
|
||||||
|
_firstGuestDrawPresented = true;
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Vulkan VideoOut presented translated guest draw: " +
|
||||||
|
$"{presentation.DrawKind}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RecordUpload(uint imageIndex)
|
||||||
|
{
|
||||||
var oldLayout = _imageInitialized[imageIndex]
|
var oldLayout = _imageInitialized[imageIndex]
|
||||||
? ImageLayout.PresentSrcKhr
|
? ImageLayout.PresentSrcKhr
|
||||||
: ImageLayout.Undefined;
|
: ImageLayout.Undefined;
|
||||||
@@ -479,7 +979,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
};
|
};
|
||||||
_vk.CmdPipelineBarrier(
|
_vk.CmdPipelineBarrier(
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
_imageInitialized[imageIndex] ? PipelineStageFlags.BottomOfPipeBit : PipelineStageFlags.TopOfPipeBit,
|
_imageInitialized[imageIndex]
|
||||||
|
? PipelineStageFlags.BottomOfPipeBit
|
||||||
|
: PipelineStageFlags.TopOfPipeBit,
|
||||||
PipelineStageFlags.TransferBit,
|
PipelineStageFlags.TransferBit,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
@@ -529,45 +1031,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
null,
|
null,
|
||||||
1,
|
1,
|
||||||
&toPresent);
|
&toPresent);
|
||||||
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer");
|
|
||||||
|
|
||||||
var waitStage = PipelineStageFlags.TransferBit;
|
|
||||||
var imageAvailable = _imageAvailable;
|
|
||||||
var commandBuffer = _commandBuffer;
|
|
||||||
var renderFinished = _renderFinished;
|
|
||||||
var submitInfo = new SubmitInfo
|
|
||||||
{
|
|
||||||
SType = StructureType.SubmitInfo,
|
|
||||||
WaitSemaphoreCount = 1,
|
|
||||||
PWaitSemaphores = &imageAvailable,
|
|
||||||
PWaitDstStageMask = &waitStage,
|
|
||||||
CommandBufferCount = 1,
|
|
||||||
PCommandBuffers = &commandBuffer,
|
|
||||||
SignalSemaphoreCount = 1,
|
|
||||||
PSignalSemaphores = &renderFinished,
|
|
||||||
};
|
|
||||||
Check(_vk.QueueSubmit(_queue, 1, &submitInfo, default), "vkQueueSubmit");
|
|
||||||
|
|
||||||
var swapchain = _swapchain;
|
|
||||||
var presentInfo = new PresentInfoKHR
|
|
||||||
{
|
|
||||||
SType = StructureType.PresentInfoKhr,
|
|
||||||
WaitSemaphoreCount = 1,
|
|
||||||
PWaitSemaphores = &renderFinished,
|
|
||||||
SwapchainCount = 1,
|
|
||||||
PSwapchains = &swapchain,
|
|
||||||
PImageIndices = &imageIndex,
|
|
||||||
};
|
|
||||||
Check(_swapchainApi.QueuePresent(_queue, &presentInfo), "vkQueuePresentKHR");
|
|
||||||
Check(_vk.QueueWaitIdle(_queue), "vkQueueWaitIdle");
|
|
||||||
_imageInitialized[imageIndex] = true;
|
|
||||||
_presentedSequence = frame.Sequence;
|
|
||||||
if (!_firstFramePresented)
|
|
||||||
{
|
|
||||||
_firstFramePresented = true;
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][INFO] Vulkan VideoOut presented first guest frame: {frame.Width}x{frame.Height}");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private Extent2D ChooseExtent(SurfaceCapabilitiesKHR capabilities)
|
private Extent2D ChooseExtent(SurfaceCapabilitiesKHR capabilities)
|
||||||
@@ -669,6 +1132,32 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
_vk.DestroySemaphore(_device, _renderFinished, null);
|
_vk.DestroySemaphore(_device, _renderFinished, null);
|
||||||
}
|
}
|
||||||
|
if (_barycentricPipeline.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyPipeline(_device, _barycentricPipeline, null);
|
||||||
|
}
|
||||||
|
if (_pipelineLayout.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyPipelineLayout(_device, _pipelineLayout, null);
|
||||||
|
}
|
||||||
|
foreach (var framebuffer in _framebuffers)
|
||||||
|
{
|
||||||
|
if (framebuffer.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyFramebuffer(_device, framebuffer, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (_renderPass.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyRenderPass(_device, _renderPass, null);
|
||||||
|
}
|
||||||
|
foreach (var imageView in _swapchainImageViews)
|
||||||
|
{
|
||||||
|
if (imageView.Handle != 0)
|
||||||
|
{
|
||||||
|
_vk.DestroyImageView(_device, imageView, null);
|
||||||
|
}
|
||||||
|
}
|
||||||
if (_commandPool.Handle != 0)
|
if (_commandPool.Handle != 0)
|
||||||
{
|
{
|
||||||
_vk.DestroyCommandPool(_device, _commandPool, null);
|
_vk.DestroyCommandPool(_device, _commandPool, null);
|
||||||
|
|||||||
Reference in New Issue
Block a user