mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35736e4740 | |||
| 92526ecacf | |||
| 0d89b2488b | |||
| c3019b78d6 | |||
| dd5e524879 | |||
| df9d0e6aaf | |||
| 253d0fae3f | |||
| 78d719ef9e | |||
| be3806cdf6 | |||
| dc47deee93 | |||
| 1c838cf8d6 | |||
| 2f269566a5 | |||
| 9d7afef6f6 | |||
| 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 | |||
| 5d204afd10 | |||
| 7bd1e8dc29 | |||
| fad61e116e | |||
| bc297fa175 | |||
| 79e767ed9f | |||
| 1ea1396979 | |||
| a1dea499bc | |||
| fee6af3136 |
Binary file not shown.
|
After Width: | Height: | Size: 933 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
Binary file not shown.
|
After Width: | Height: | Size: 89 KiB |
@@ -9,11 +9,17 @@ on:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "**/*.png"
|
||||
- "**/*.jpeg"
|
||||
- "**/*.jpg"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "**/*.png"
|
||||
- "**/*.jpeg"
|
||||
- "**/*.jpg"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -120,7 +126,7 @@ jobs:
|
||||
needs:
|
||||
- init
|
||||
- build
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -9,5 +9,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
+2
-1
@@ -7,7 +7,8 @@ path = [
|
||||
"**/packages.lock.json",
|
||||
"scripts/ps5_names.txt",
|
||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||
"_logs/**"
|
||||
"_logs/**",
|
||||
".github/images/**"
|
||||
]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||
|
||||
@@ -8,6 +8,62 @@
|
||||
"resolved": "10.0.3",
|
||||
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.9",
|
||||
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||
},
|
||||
"Silk.NET.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||
}
|
||||
},
|
||||
"Silk.NET.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Ultz.Native.GLFW": "3.4.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Maths": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||
},
|
||||
"Silk.NET.Windowing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Maths": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||
"dependencies": {
|
||||
"Silk.NET.GLFW": "2.23.0",
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.core": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
@@ -23,7 +79,10 @@
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )"
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
@@ -34,10 +93,57 @@
|
||||
"requested": "[1.21.0, )",
|
||||
"resolved": "1.21.0",
|
||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
}
|
||||
},
|
||||
"net10.0/linux-x64": {},
|
||||
"net10.0/osx-arm64": {},
|
||||
"net10.0/win-x64": {}
|
||||
"net10.0/linux-x64": {
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/osx-arm64": {
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/win-x64": {
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -22,6 +22,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
private const ulong StackSize = 0x0020_0000UL;
|
||||
private const ulong TlsBaseAddress = 0x7FFE_0000_0000UL;
|
||||
private const ulong TlsSize = 0x0001_0000UL;
|
||||
private const ulong TlsPrefixSize = 0x0000_1000UL;
|
||||
private const ulong BootstrapStubBaseAddress = 0x7FFD_F000_0000UL;
|
||||
private const ulong BootstrapPayloadBaseAddress = 0x7FFD_E000_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++)
|
||||
{
|
||||
var candidateBase = TlsBaseAddress - ((ulong)i * tlsStride);
|
||||
var mappedBase = candidateBase - TlsPrefixSize;
|
||||
try
|
||||
{
|
||||
_virtualMemory.Map(
|
||||
candidateBase,
|
||||
TlsSize,
|
||||
mappedBase,
|
||||
TlsSize + TlsPrefixSize,
|
||||
fileOffset: 0,
|
||||
fileData: ReadOnlySpan<byte>.Empty,
|
||||
ProgramHeaderFlags.Read | ProgramHeaderFlags.Write);
|
||||
@@ -363,9 +365,11 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
|
||||
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 + 0x28, 0xC0DEC0DECAFEBABEUL);
|
||||
context.TryWriteUInt64(tlsBase + 0x28, 0xC0DEC0DECAFEBABEUL) &&
|
||||
context.TryWriteUInt64(tlsBase + 0x60, tlsBase);
|
||||
}
|
||||
|
||||
private static bool InitializeGuestFrameChainSentinel(CpuContext context)
|
||||
@@ -420,6 +424,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
var entryStackPointer = entryParamsAddress - sizeof(ulong);
|
||||
if (!context.TryWriteUInt64(entryStackPointer, 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
context[CpuRegister.Rsp] = entryStackPointer;
|
||||
context[CpuRegister.Rdi] = entryParamsAddress;
|
||||
context[CpuRegister.Rsi] = programExitHandlerAddress;
|
||||
context[CpuRegister.Rdx] = 0;
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -12,10 +13,23 @@ namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
|
||||
|
||||
private void RecordRecentImportTrace(string traceLine)
|
||||
private void RecordRecentImportTrace(
|
||||
long dispatchIndex,
|
||||
string nid,
|
||||
ulong returnRip,
|
||||
ulong arg0,
|
||||
ulong arg1,
|
||||
ulong arg2)
|
||||
{
|
||||
_recentImportTrace[_recentImportTraceWriteIndex] = traceLine;
|
||||
_recentImportTrace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
|
||||
dispatchIndex,
|
||||
nid,
|
||||
returnRip,
|
||||
arg0,
|
||||
arg1,
|
||||
arg2);
|
||||
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
|
||||
if (_recentImportTraceCount < _recentImportTrace.Length)
|
||||
{
|
||||
@@ -34,10 +48,12 @@ public sealed partial class DirectExecutionBackend
|
||||
for (int i = 0; i < _recentImportTraceCount; i++)
|
||||
{
|
||||
int num2 = (num + i) % _recentImportTrace.Length;
|
||||
string text = _recentImportTrace[num2];
|
||||
if (!string.IsNullOrEmpty(text))
|
||||
var entry = _recentImportTrace[num2];
|
||||
if (!string.IsNullOrEmpty(entry.Nid))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] " + text);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -97,12 +113,13 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
private void ProbeReturnRip(ulong returnRip, long dispatchIndex)
|
||||
{
|
||||
if (_cpuContext == null || returnRip == 0)
|
||||
var cpuContext = ActiveCpuContext;
|
||||
if (cpuContext == null || returnRip == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
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}");
|
||||
return;
|
||||
@@ -113,7 +130,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
int num = BitConverter.ToInt32(destination.Slice(2, 4));
|
||||
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}");
|
||||
}
|
||||
@@ -122,7 +139,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
int num3 = BitConverter.ToInt32(destination.Slice(3, 4));
|
||||
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}");
|
||||
}
|
||||
@@ -134,12 +151,82 @@ public sealed partial class DirectExecutionBackend
|
||||
int num5 = BitConverter.ToInt32(destination.Slice(i + 2, 4));
|
||||
ulong num6 = returnRip + (ulong)i;
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
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)
|
||||
@@ -231,11 +318,24 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
private unsafe static bool IsExecutableAddress(ulong address)
|
||||
{
|
||||
var pageAddress = address & ~0xFFFUL;
|
||||
if (_knownExecutablePages.ContainsKey(pageAddress))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
return lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
||||
|
||||
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
||||
if (executable)
|
||||
{
|
||||
_knownExecutablePages.TryAdd(pageAddress, 0);
|
||||
}
|
||||
|
||||
return executable;
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment)
|
||||
|
||||
@@ -7,6 +7,7 @@ using System.Collections.Generic;
|
||||
using System.Globalization;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.Core.Cpu.Disasm;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
@@ -14,11 +15,19 @@ namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
|
||||
private static int _lazyCommitTraceCount;
|
||||
|
||||
private unsafe void SetupExceptionHandler()
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, RawVectoredHandlerPtrManaged);
|
||||
_rawExceptionHandlerStub = CreateExceptionHandlerTrampoline(RawVectoredHandlerPtrManaged);
|
||||
if (_rawExceptionHandlerStub == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create raw exception handler trampoline");
|
||||
}
|
||||
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
|
||||
}
|
||||
else
|
||||
@@ -28,12 +37,22 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
_handlerDelegate = VectoredHandler;
|
||||
_handlerHandle = GCHandle.Alloc(_handlerDelegate);
|
||||
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
||||
_exceptionHandlerStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_handlerDelegate));
|
||||
if (_exceptionHandlerStub == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create exception handler trampoline");
|
||||
}
|
||||
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
|
||||
|
||||
_unhandledFilterDelegate = UnhandledExceptionFilter;
|
||||
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
|
||||
SetUnhandledExceptionFilter(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
||||
_unhandledFilterStub = CreateExceptionHandlerTrampoline(Marshal.GetFunctionPointerForDelegate(_unhandledFilterDelegate));
|
||||
if (_unhandledFilterStub == 0)
|
||||
{
|
||||
throw new InvalidOperationException("Failed to create unhandled exception filter trampoline");
|
||||
}
|
||||
SetUnhandledExceptionFilter(_unhandledFilterStub);
|
||||
}
|
||||
|
||||
private unsafe int UnhandledExceptionFilter(void* exceptionInfo)
|
||||
@@ -61,7 +80,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
if (_vectoredHandlerDepth > 0)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][TRACE] Nested VEH exception; passing through.");
|
||||
LogNestedVectoredException(exceptionInfo);
|
||||
Console.Error.Flush();
|
||||
return 0;
|
||||
}
|
||||
@@ -84,10 +103,18 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||
|
||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord))
|
||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (IsBenignHostDebugException(exceptionCode))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
if (exceptionCode == MSVC_CPP_EXCEPTION)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
ulong accessType = exceptionRecord->NumberParameters >= 1 ? (*exceptionRecord->ExceptionInformation) : 0;
|
||||
@@ -828,7 +886,7 @@ public sealed partial class DirectExecutionBackend
|
||||
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)
|
||||
{
|
||||
@@ -845,6 +903,10 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (!IsGuestOwnedLazyCommitAddress(faultAddress, out var owner))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (VirtualQuery((void*)faultAddress, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||
{
|
||||
return false;
|
||||
@@ -852,7 +914,21 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query: fault=0x{faultAddress:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
||||
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
||||
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
||||
}
|
||||
|
||||
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
|
||||
{
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool committed = false;
|
||||
ulong committedBase = 0;
|
||||
@@ -860,14 +936,25 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
if (mbi.State == 65536)
|
||||
{
|
||||
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
||||
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = largeBase;
|
||||
committedSize = 2097152uL;
|
||||
committedBase = windowBase;
|
||||
committedSize = windowSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = largeBase;
|
||||
committedSize = 2097152uL;
|
||||
}
|
||||
}
|
||||
|
||||
if (!committed)
|
||||
{
|
||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||
@@ -890,7 +977,10 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
|
||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -899,14 +989,25 @@ public sealed partial class DirectExecutionBackend
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
||||
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
||||
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = largeCommitBase;
|
||||
committedSize = 2097152uL;
|
||||
committedBase = commitWindowBase;
|
||||
committedSize = commitWindowSize;
|
||||
}
|
||||
else
|
||||
{
|
||||
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
||||
{
|
||||
committed = true;
|
||||
committedBase = largeCommitBase;
|
||||
committedSize = 2097152uL;
|
||||
}
|
||||
}
|
||||
|
||||
if (!committed)
|
||||
{
|
||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
||||
@@ -935,9 +1036,46 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
|
||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||
if (traceLazyCommit)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||
}
|
||||
return true;
|
||||
|
||||
static bool TryGetLazyCommitWindow(ulong fault, ulong regionBase, ulong regionSize, out ulong baseAddress, out ulong length)
|
||||
{
|
||||
baseAddress = 0;
|
||||
length = 0;
|
||||
if (regionSize == 0 || ulong.MaxValue - regionBase < regionSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong regionEnd = regionBase + regionSize;
|
||||
ulong windowBase = fault & ~(LazyCommitWindowBytes - 1);
|
||||
if (windowBase < regionBase)
|
||||
{
|
||||
windowBase = regionBase;
|
||||
}
|
||||
|
||||
if (windowBase >= regionEnd)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ulong windowEnd = Math.Min(regionEnd, windowBase + LazyCommitWindowBytes);
|
||||
ulong windowSize = windowEnd - windowBase;
|
||||
windowSize &= 0xFFFFFFFFFFFFF000uL;
|
||||
if (windowSize == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
baseAddress = windowBase;
|
||||
length = windowSize;
|
||||
return true;
|
||||
}
|
||||
|
||||
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
||||
{
|
||||
if (length == 0)
|
||||
@@ -964,6 +1102,49 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
return TryCommitRange(commitAddress, commitSize, protection);
|
||||
}
|
||||
|
||||
static bool IsAccessCompatible(ulong accessType, uint protection)
|
||||
{
|
||||
const uint pageNoAccess = 0x01;
|
||||
const uint pageReadOnly = 0x02;
|
||||
const uint pageReadWrite = 0x04;
|
||||
const uint pageWriteCopy = 0x08;
|
||||
const uint pageExecute = 0x10;
|
||||
const uint pageExecuteRead = 0x20;
|
||||
const uint pageExecuteReadWrite = 0x40;
|
||||
const uint pageExecuteWriteCopy = 0x80;
|
||||
const uint pageGuard = 0x100;
|
||||
const uint accessMask = 0xFF;
|
||||
|
||||
if ((protection & pageGuard) != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
uint access = protection & accessMask;
|
||||
if (access == pageNoAccess)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return accessType switch
|
||||
{
|
||||
0 => access is pageReadOnly or pageReadWrite or pageWriteCopy or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
|
||||
1 => access is pageReadWrite or pageWriteCopy or pageExecuteReadWrite or pageExecuteWriteCopy,
|
||||
8 => access is pageExecute or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
|
||||
_ => false
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldTraceLazyCommit(int traceIndex)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LAZY_COMMIT"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return traceIndex <= 16 || traceIndex % 256 == 0;
|
||||
}
|
||||
|
||||
private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
|
||||
|
||||
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;
|
||||
|
||||
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory
|
||||
public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemoryAllocator
|
||||
{
|
||||
private readonly ICpuMemory _inner;
|
||||
|
||||
@@ -39,4 +39,15 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory
|
||||
|
||||
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,
|
||||
ulong initFunctionEntryPoint = 0,
|
||||
ulong imageBase = 0,
|
||||
ulong procParamAddress = 0)
|
||||
ulong procParamAddress = 0,
|
||||
string? title = null,
|
||||
string? titleId = null,
|
||||
string? version = null)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(programHeaders);
|
||||
ArgumentNullException.ThrowIfNull(mappedRegions);
|
||||
@@ -38,6 +41,9 @@ public sealed class SelfImage
|
||||
InitFunctionEntryPoint = initFunctionEntryPoint;
|
||||
_imageBase = imageBase;
|
||||
ProcParamAddress = procParamAddress;
|
||||
Title = title;
|
||||
TitleId = titleId;
|
||||
Version = version;
|
||||
}
|
||||
|
||||
public bool IsSelf { get; }
|
||||
@@ -63,4 +69,10 @@ public sealed class SelfImage
|
||||
public ulong EntryPoint => ElfHeader.EntryPoint + _imageBase;
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
if (readParamJson)
|
||||
{
|
||||
TryLoadParamJson(fs, mountRoot);
|
||||
}
|
||||
var applicationInfo = readParamJson
|
||||
? TryLoadParamJson(fs, mountRoot)
|
||||
: default;
|
||||
|
||||
if (clearVirtualMemory)
|
||||
{
|
||||
@@ -270,15 +269,20 @@ public sealed class SelfLoader : ISelfLoader
|
||||
initializerFunctions,
|
||||
initFunctionEntryPoint,
|
||||
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)
|
||||
{
|
||||
Console.WriteLine("[LOADER] param.json not found (no filesystem provided).");
|
||||
return;
|
||||
return default;
|
||||
}
|
||||
|
||||
string[] possiblePaths = string.IsNullOrEmpty(mountRoot)
|
||||
@@ -298,12 +302,16 @@ public sealed class SelfLoader : ISelfLoader
|
||||
if (foundPath == null)
|
||||
{
|
||||
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] 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)
|
||||
|
||||
@@ -7,13 +7,17 @@ using SharpEmu.HLE;
|
||||
|
||||
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 Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||
private bool _disposed;
|
||||
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 LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
|
||||
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_READONLY = 0x02;
|
||||
|
||||
private ulong _guestAllocationArenaBase;
|
||||
private ulong _guestAllocationOffset;
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
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;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
{
|
||||
@@ -82,6 +90,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
Protection = protection
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
var allocationKind = executable ? "executable memory" : "data memory";
|
||||
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
|
||||
{
|
||||
@@ -206,6 +219,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
Protection = protection
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
var allocationKind = reservedOnly
|
||||
? "reserved data memory (lazy commit)"
|
||||
@@ -215,16 +232,116 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
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()
|
||||
{
|
||||
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();
|
||||
_pageProtections.Clear();
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
_guestAllocationArenaBase = 0;
|
||||
_guestAllocationOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +358,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
var mapEnd = AlignUp(segmentEnd, PageSize);
|
||||
var mapSize = checked(mapEnd - mapStart);
|
||||
|
||||
lock (_gate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
var existingRegion = FindRegion(mapStart, mapSize);
|
||||
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})");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
private void ApplySegmentProtection(ulong mapStart, ulong mapEnd, ProgramHeaderFlags flags)
|
||||
@@ -323,7 +445,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
|
||||
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
||||
for (var i = 0; i < _regions.Count; i++)
|
||||
@@ -338,11 +461,17 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
lock (_gate)
|
||||
var requiresExclusiveAccess = false;
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
@@ -354,38 +483,55 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
|
||||
if (!requiresExclusiveAccess)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
return TryReadExclusive(virtualAddress, destination);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
lock (_gate)
|
||||
var requiresExclusiveAccess = false;
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
@@ -397,37 +543,148 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* srcPtr = source)
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
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 _);
|
||||
if (IsExecutableProtection(oldProtect))
|
||||
{
|
||||
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
||||
}
|
||||
requiresExclusiveAccess = true;
|
||||
break;
|
||||
}
|
||||
|
||||
fixed (byte* srcPtr = source)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
|
||||
if (!requiresExclusiveAccess)
|
||||
{
|
||||
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)
|
||||
@@ -439,7 +696,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
|
||||
public void* GetPointer(ulong virtualAddress)
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
@@ -451,11 +709,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
}
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAccessible(ulong virtualAddress, ulong size)
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
@@ -466,6 +729,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
}
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
private MemoryRegion? FindRegion(ulong address, ulong size)
|
||||
@@ -480,6 +747,35 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
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)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
private bool CanReadWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
|
||||
CanAccessWithoutProtectionChange(address, size, region, write: false);
|
||||
|
||||
private bool CanWriteWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
|
||||
CanAccessWithoutProtectionChange(address, size, region, write: true);
|
||||
|
||||
private bool CanAccessWithoutProtectionChange(ulong address, ulong size, MemoryRegion region, bool write)
|
||||
{
|
||||
var startPage = AlignDown(address, PageSize);
|
||||
var endPage = AlignUp(address + size, PageSize);
|
||||
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
||||
{
|
||||
if (_pageProtections.TryGetValue(pageAddress, out var flags))
|
||||
{
|
||||
if (write ? (flags & ProgramHeaderFlags.Write) == 0 : (flags & ProgramHeaderFlags.Read) == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if (write ? !IsWritableProtection(region.Protection) : !IsReadableProtection(region.Protection))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsReadableProtection(uint protection)
|
||||
{
|
||||
return protection is PAGE_READONLY or PAGE_READWRITE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE;
|
||||
}
|
||||
|
||||
private static bool IsWritableProtection(uint protection)
|
||||
{
|
||||
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
|
||||
}
|
||||
|
||||
private static uint GetCommitProtection(MemoryRegion region)
|
||||
{
|
||||
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
@@ -523,15 +857,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
var endPage = AlignUp(address + size, PageSize);
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
pageAddress = rangeEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -540,10 +885,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
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;
|
||||
}
|
||||
|
||||
pageAddress = rangeEnd;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
@@ -10,6 +10,7 @@ using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.AppContent;
|
||||
using SharpEmu.Libs.SaveData;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -135,6 +136,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
LastMilestoneLog = null;
|
||||
KernelModuleRegistry.Reset();
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
||||
|
||||
@@ -8,17 +8,105 @@
|
||||
"resolved": "1.21.0",
|
||||
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.9",
|
||||
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||
},
|
||||
"Silk.NET.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||
}
|
||||
},
|
||||
"Silk.NET.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Ultz.Native.GLFW": "3.4.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Maths": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||
},
|
||||
"Silk.NET.Windowing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Maths": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||
"dependencies": {
|
||||
"Silk.NET.GLFW": "2.23.0",
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )"
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public readonly record struct GuestThreadStartRequest(
|
||||
@@ -8,46 +10,165 @@ public readonly record struct GuestThreadStartRequest(
|
||||
ulong EntryPoint,
|
||||
ulong Argument,
|
||||
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
|
||||
{
|
||||
bool SupportsGuestContextTransfer { get; }
|
||||
|
||||
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
||||
|
||||
bool TryJoinThread(
|
||||
CpuContext callerContext,
|
||||
ulong threadHandle,
|
||||
out ulong returnValue,
|
||||
out string? error);
|
||||
|
||||
void Pump(CpuContext callerContext, string reason);
|
||||
|
||||
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
|
||||
{
|
||||
[ThreadStatic]
|
||||
private static ulong _currentGuestThreadHandle;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong _currentFiberAddress;
|
||||
|
||||
[ThreadStatic]
|
||||
private static string? _pendingBlockReason;
|
||||
|
||||
[ThreadStatic]
|
||||
private static bool _pendingBlockContinuationValid;
|
||||
|
||||
[ThreadStatic]
|
||||
private static GuestCpuContinuation _pendingBlockContinuation;
|
||||
|
||||
[ThreadStatic]
|
||||
private static string? _pendingBlockWakeKey;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Func<int>? _pendingBlockResumeHandler;
|
||||
|
||||
[ThreadStatic]
|
||||
private static Func<bool>? _pendingBlockWakeHandler;
|
||||
|
||||
[ThreadStatic]
|
||||
private static long _pendingBlockDeadlineTimestamp;
|
||||
|
||||
[ThreadStatic]
|
||||
private static bool _pendingEntryExit;
|
||||
|
||||
[ThreadStatic]
|
||||
private static int _pendingEntryExitStatus;
|
||||
private static ulong _pendingEntryExitValue;
|
||||
|
||||
[ThreadStatic]
|
||||
private static string? _pendingEntryExitReason;
|
||||
|
||||
[ThreadStatic]
|
||||
private static bool _pendingContextTransfer;
|
||||
|
||||
[ThreadStatic]
|
||||
private static GuestCpuContinuation _pendingContextTransferTarget;
|
||||
|
||||
[ThreadStatic]
|
||||
private static bool _hasCurrentImportCallFrame;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong _currentImportReturnRip;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong _currentImportResumeRsp;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong _currentImportReturnSlotAddress;
|
||||
|
||||
public static IGuestThreadScheduler? Scheduler { get; set; }
|
||||
|
||||
public static bool IsGuestThread => _currentGuestThreadHandle != 0;
|
||||
|
||||
public static ulong CurrentGuestThreadHandle => _currentGuestThreadHandle;
|
||||
|
||||
public static ulong CurrentFiberAddress => _currentFiberAddress;
|
||||
|
||||
public static ulong EnterGuestThread(ulong threadHandle)
|
||||
{
|
||||
var previous = _currentGuestThreadHandle;
|
||||
_currentGuestThreadHandle = threadHandle;
|
||||
_pendingBlockReason = null;
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockResumeHandler = null;
|
||||
_pendingBlockWakeHandler = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
_pendingEntryExit = false;
|
||||
_pendingEntryExitStatus = 0;
|
||||
_pendingEntryExitValue = 0;
|
||||
_pendingEntryExitReason = null;
|
||||
_pendingContextTransfer = false;
|
||||
_pendingContextTransferTarget = default;
|
||||
_hasCurrentImportCallFrame = false;
|
||||
_currentImportReturnRip = 0;
|
||||
_currentImportResumeRsp = 0;
|
||||
_currentImportReturnSlotAddress = 0;
|
||||
return previous;
|
||||
}
|
||||
|
||||
@@ -55,12 +176,44 @@ public static class GuestThreadExecution
|
||||
{
|
||||
_currentGuestThreadHandle = previousThreadHandle;
|
||||
_pendingBlockReason = null;
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockResumeHandler = null;
|
||||
_pendingBlockWakeHandler = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
_pendingEntryExit = false;
|
||||
_pendingEntryExitStatus = 0;
|
||||
_pendingEntryExitValue = 0;
|
||||
_pendingEntryExitReason = null;
|
||||
_pendingContextTransfer = false;
|
||||
_pendingContextTransferTarget = default;
|
||||
_hasCurrentImportCallFrame = false;
|
||||
_currentImportReturnRip = 0;
|
||||
_currentImportResumeRsp = 0;
|
||||
_currentImportReturnSlotAddress = 0;
|
||||
}
|
||||
|
||||
public static bool RequestCurrentThreadBlock(string reason)
|
||||
public static ulong EnterFiber(ulong fiberAddress)
|
||||
{
|
||||
var previous = _currentFiberAddress;
|
||||
_currentFiberAddress = fiberAddress;
|
||||
return previous;
|
||||
}
|
||||
|
||||
public static void RestoreFiber(ulong previousFiberAddress)
|
||||
{
|
||||
_currentFiberAddress = previousFiberAddress;
|
||||
}
|
||||
|
||||
public static bool RequestCurrentThreadBlock(string reason) => RequestCurrentThreadBlock(null, reason);
|
||||
|
||||
public static bool RequestCurrentThreadBlock(
|
||||
CpuContext? context,
|
||||
string reason,
|
||||
string? wakeKey = null,
|
||||
Func<int>? resumeHandler = null,
|
||||
Func<bool>? wakeHandler = null,
|
||||
long blockDeadlineTimestamp = 0)
|
||||
{
|
||||
if (!IsGuestThread)
|
||||
{
|
||||
@@ -68,31 +221,167 @@ public static class GuestThreadExecution
|
||||
}
|
||||
|
||||
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
||||
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
||||
_pendingBlockResumeHandler = resumeHandler;
|
||||
_pendingBlockWakeHandler = wakeHandler;
|
||||
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
||||
{
|
||||
_pendingBlockContinuation = continuation;
|
||||
_pendingBlockContinuationValid = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockContinuationValid = false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public static bool TryConsumeCurrentThreadBlock(out string reason)
|
||||
{
|
||||
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
|
||||
}
|
||||
|
||||
public static bool TryConsumeCurrentThreadBlock(
|
||||
out string reason,
|
||||
out GuestCpuContinuation continuation,
|
||||
out bool hasContinuation)
|
||||
{
|
||||
return TryConsumeCurrentThreadBlock(
|
||||
out reason,
|
||||
out continuation,
|
||||
out hasContinuation,
|
||||
out _,
|
||||
out _,
|
||||
out _,
|
||||
out _);
|
||||
}
|
||||
|
||||
public static bool TryConsumeCurrentThreadBlock(
|
||||
out string reason,
|
||||
out GuestCpuContinuation continuation,
|
||||
out bool hasContinuation,
|
||||
out string wakeKey,
|
||||
out Func<int>? resumeHandler,
|
||||
out Func<bool>? wakeHandler)
|
||||
{
|
||||
return TryConsumeCurrentThreadBlock(
|
||||
out reason,
|
||||
out continuation,
|
||||
out hasContinuation,
|
||||
out wakeKey,
|
||||
out resumeHandler,
|
||||
out wakeHandler,
|
||||
out _);
|
||||
}
|
||||
|
||||
public static bool TryConsumeCurrentThreadBlock(
|
||||
out string reason,
|
||||
out GuestCpuContinuation continuation,
|
||||
out bool hasContinuation,
|
||||
out string wakeKey,
|
||||
out Func<int>? resumeHandler,
|
||||
out Func<bool>? wakeHandler,
|
||||
out long blockDeadlineTimestamp)
|
||||
{
|
||||
reason = _pendingBlockReason ?? string.Empty;
|
||||
if (string.IsNullOrEmpty(reason))
|
||||
{
|
||||
continuation = default;
|
||||
hasContinuation = false;
|
||||
wakeKey = string.Empty;
|
||||
resumeHandler = null;
|
||||
wakeHandler = null;
|
||||
blockDeadlineTimestamp = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
continuation = _pendingBlockContinuation;
|
||||
hasContinuation = _pendingBlockContinuationValid;
|
||||
wakeKey = _pendingBlockWakeKey ?? reason;
|
||||
resumeHandler = _pendingBlockResumeHandler;
|
||||
wakeHandler = _pendingBlockWakeHandler;
|
||||
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
||||
_pendingBlockReason = null;
|
||||
_pendingBlockContinuation = default;
|
||||
_pendingBlockContinuationValid = false;
|
||||
_pendingBlockWakeKey = null;
|
||||
_pendingBlockResumeHandler = null;
|
||||
_pendingBlockWakeHandler = null;
|
||||
_pendingBlockDeadlineTimestamp = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
|
||||
{
|
||||
if (timeout <= TimeSpan.Zero)
|
||||
{
|
||||
return Stopwatch.GetTimestamp();
|
||||
}
|
||||
|
||||
var ticks = timeout.TotalSeconds >= long.MaxValue / (double)Stopwatch.Frequency
|
||||
? long.MaxValue
|
||||
: (long)Math.Ceiling(timeout.TotalSeconds * Stopwatch.Frequency);
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
if (long.MaxValue - now <= ticks)
|
||||
{
|
||||
return long.MaxValue;
|
||||
}
|
||||
|
||||
return now + Math.Max(1, ticks);
|
||||
}
|
||||
|
||||
private static bool TryCaptureCurrentBlockContinuation(CpuContext context, out GuestCpuContinuation continuation)
|
||||
{
|
||||
if (!TryGetCurrentImportCallFrame(out var frame) ||
|
||||
frame.ReturnRip < 65536 ||
|
||||
frame.ResumeRsp == 0 ||
|
||||
frame.ReturnSlotAddress == 0)
|
||||
{
|
||||
continuation = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
continuation = new GuestCpuContinuation(
|
||||
frame.ReturnRip,
|
||||
frame.ResumeRsp,
|
||||
frame.ReturnSlotAddress,
|
||||
context.Rflags,
|
||||
context.FsBase,
|
||||
context.GsBase,
|
||||
0,
|
||||
context[CpuRegister.Rcx],
|
||||
context[CpuRegister.Rdx],
|
||||
context[CpuRegister.Rbx],
|
||||
context[CpuRegister.Rbp],
|
||||
context[CpuRegister.Rsi],
|
||||
context[CpuRegister.Rdi],
|
||||
context[CpuRegister.R8],
|
||||
context[CpuRegister.R9],
|
||||
context[CpuRegister.R12],
|
||||
context[CpuRegister.R13],
|
||||
context[CpuRegister.R14],
|
||||
context[CpuRegister.R15]);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static void RequestCurrentEntryExit(string reason, int status)
|
||||
{
|
||||
RequestCurrentEntryExit(reason, unchecked((ulong)(long)status));
|
||||
}
|
||||
|
||||
public static void RequestCurrentEntryExit(string reason, ulong value)
|
||||
{
|
||||
_pendingEntryExit = true;
|
||||
_pendingEntryExitStatus = status;
|
||||
_pendingEntryExitValue = value;
|
||||
_pendingEntryExitReason = string.IsNullOrWhiteSpace(reason) ? "guest_entry_exit" : reason;
|
||||
}
|
||||
|
||||
public static bool TryConsumeCurrentEntryExit(out int status, out string reason)
|
||||
public static bool TryConsumeCurrentEntryExit(out ulong value, out string reason)
|
||||
{
|
||||
status = _pendingEntryExitStatus;
|
||||
value = _pendingEntryExitValue;
|
||||
reason = _pendingEntryExitReason ?? string.Empty;
|
||||
if (!_pendingEntryExit)
|
||||
{
|
||||
@@ -100,8 +389,68 @@ public static class GuestThreadExecution
|
||||
}
|
||||
|
||||
_pendingEntryExit = false;
|
||||
_pendingEntryExitStatus = 0;
|
||||
_pendingEntryExitValue = 0;
|
||||
_pendingEntryExitReason = null;
|
||||
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);
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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,8 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
@@ -17,9 +19,16 @@ public static class AmprExports
|
||||
private const ulong CommandBufferAux1Offset = 0x20;
|
||||
private const ulong ReadFileRecordSize = 0x30;
|
||||
private const ulong KernelEventQueueRecordSize = 0x30;
|
||||
private const ulong WriteAddressRecordSize = 0x20;
|
||||
private const uint ReadFileRecordType = 1;
|
||||
private const uint KernelEventQueueRecordType = 2;
|
||||
private const uint WriteAddressRecordType = 3;
|
||||
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
||||
private static readonly bool _traceAmpr =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||
private static readonly bool _traceAmprReads =
|
||||
_traceAmpr ||
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
|
||||
|
||||
private sealed class CommandBufferState
|
||||
{
|
||||
@@ -72,12 +81,7 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var buffer = 0UL;
|
||||
var size = 0UL;
|
||||
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer);
|
||||
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size);
|
||||
|
||||
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0, aux1, clear: false))
|
||||
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer: 0, size: 0, aux0, aux1, clear: false))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -101,8 +105,9 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, 0) ||
|
||||
!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, 0))
|
||||
Span<byte> auxiliaryPointers = stackalloc byte[sizeof(ulong) * 2];
|
||||
auxiliaryPointers.Clear();
|
||||
if (!ctx.Memory.TryWrite(commandBuffer + CommandBufferAux0Offset, auxiliaryPointers))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -176,8 +181,15 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out var buffer) ||
|
||||
!ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out var size) ||
|
||||
Span<byte> bufferPointers = stackalloc byte[sizeof(ulong) * 2];
|
||||
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))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -284,6 +296,18 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "C+IEj+BsAFM",
|
||||
ExportName = "sceAmprMeasureCommandSizeWriteAddressOnCompletion",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int MeasureCommandSizeWriteAddressOnCompletion(CpuContext ctx)
|
||||
{
|
||||
TraceAmpr(ctx, "measure_write_address_complete", 0, WriteAddressRecordSize, 0);
|
||||
ctx[CpuRegister.Rax] = WriteAddressRecordSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "tZDDEo2tE5k",
|
||||
ExportName = "sceAmprCommandBufferGetSize",
|
||||
@@ -340,28 +364,116 @@ public static class AmprExports
|
||||
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||
var equeue = ctx[CpuRegister.Rsi];
|
||||
var ident = ctx[CpuRegister.Rdx];
|
||||
var filter = ctx[CpuRegister.Rcx];
|
||||
var completionToken = ctx[CpuRegister.Rcx];
|
||||
var userData = ctx[CpuRegister.R8];
|
||||
var data = ctx[CpuRegister.R9];
|
||||
|
||||
if (commandBuffer == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var extra = 0UL;
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out extra);
|
||||
|
||||
if (!AppendKernelEventQueueRecord(ctx, commandBuffer, equeue, ident, filter, userData, data, extra))
|
||||
if (!AppendKernelEventQueueRecord(
|
||||
ctx,
|
||||
commandBuffer,
|
||||
equeue,
|
||||
ident,
|
||||
completionToken,
|
||||
userData))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
TraceAmpr(ctx, "write_equeue", commandBuffer, equeue, ident);
|
||||
TraceAmpr(ctx, "write_equeue", commandBuffer, ident, completionToken);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sJXyWHjP-F8",
|
||||
ExportName = "sceAmprCommandBufferWriteAddressOnCompletion",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAmpr")]
|
||||
public static int CommandBufferWriteAddressOnCompletion(CpuContext ctx)
|
||||
{
|
||||
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||
var address = ctx[CpuRegister.Rsi];
|
||||
var value = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (commandBuffer == 0 || address == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!AppendWriteAddressRecord(ctx, commandBuffer, address, value))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
TraceAmpr(ctx, "write_address_complete", commandBuffer, address, value);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
public static int CompleteCommandBuffer(CpuContext ctx, ulong commandBuffer)
|
||||
{
|
||||
if (commandBuffer == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryGetCommandBufferState(ctx, commandBuffer, out var buffer, out _, out var state) || state is null)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ulong writeOffset;
|
||||
lock (state)
|
||||
{
|
||||
writeOffset = state.WriteOffset;
|
||||
}
|
||||
|
||||
var offset = 0UL;
|
||||
while (offset < writeOffset)
|
||||
{
|
||||
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
switch (recordType)
|
||||
{
|
||||
case ReadFileRecordType:
|
||||
offset += ReadFileRecordSize;
|
||||
break;
|
||||
|
||||
case KernelEventQueueRecordType:
|
||||
if (!CompleteKernelEventQueueRecord(ctx, buffer + offset))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
offset += KernelEventQueueRecordSize;
|
||||
break;
|
||||
|
||||
case WriteAddressRecordType:
|
||||
if (!CompleteWriteAddressRecord(ctx, buffer + offset))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
offset += WriteAddressRecordSize;
|
||||
break;
|
||||
|
||||
default:
|
||||
TraceAmpr(ctx, "complete_unknown", commandBuffer, recordType, offset);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
TraceAmpr(ctx, "complete", commandBuffer, buffer, writeOffset);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool InitializeCommandBuffer(
|
||||
CpuContext ctx,
|
||||
ulong commandBuffer,
|
||||
@@ -371,20 +483,34 @@ public static class AmprExports
|
||||
ulong aux1,
|
||||
bool clear)
|
||||
{
|
||||
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
|
||||
if (clear)
|
||||
{
|
||||
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
|
||||
header.Clear();
|
||||
if (!ctx.Memory.TryWrite(commandBuffer, header))
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ctx.Memory.TryRead(commandBuffer, header))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferDataOffset..]);
|
||||
size = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferSizeOffset..]);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
|
||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, aux0) &&
|
||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, aux1) &&
|
||||
WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSelfOffset..], commandBuffer);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferDataOffset..], buffer);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSizeOffset..], size);
|
||||
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)
|
||||
@@ -399,6 +525,26 @@ public static class AmprExports
|
||||
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());
|
||||
lock (state)
|
||||
{
|
||||
@@ -406,15 +552,6 @@ public static class AmprExports
|
||||
state.Size = size;
|
||||
state.WriteOffset = writeOffset;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
||||
{
|
||||
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
|
||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferDataOffset, buffer) &&
|
||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferSizeOffset, size);
|
||||
}
|
||||
|
||||
private static bool TryGetCommandBufferState(
|
||||
@@ -435,9 +572,11 @@ public static class AmprExports
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer) &&
|
||||
ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size))
|
||||
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 2];
|
||||
if (ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, pointers))
|
||||
{
|
||||
buffer = BinaryPrimitives.ReadUInt64LittleEndian(pointers);
|
||||
size = BinaryPrimitives.ReadUInt64LittleEndian(pointers[sizeof(ulong)..]);
|
||||
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
||||
lock (state)
|
||||
{
|
||||
@@ -490,12 +629,18 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const int ChunkSize = 64 * 1024;
|
||||
var buffer = new byte[(int)Math.Min((ulong)ChunkSize, size)];
|
||||
const int ChunkSize = 1024 * 1024;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ChunkSize, size));
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||
using var stream = new FileStream(
|
||||
hostPath,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite | FileShare.Delete,
|
||||
ChunkSize,
|
||||
FileOptions.SequentialScan);
|
||||
if (fileOffset >= (ulong)stream.Length)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -528,6 +673,10 @@ public static class AmprExports
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -558,20 +707,28 @@ public static class AmprExports
|
||||
ulong commandBuffer,
|
||||
ulong equeue,
|
||||
ulong ident,
|
||||
ulong filter,
|
||||
ulong userData,
|
||||
ulong data,
|
||||
ulong extra)
|
||||
ulong completionToken,
|
||||
ulong userData)
|
||||
{
|
||||
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
|
||||
record.Clear();
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], unchecked((uint)filter));
|
||||
BinaryPrimitives.WriteInt16LittleEndian(record[0x04..], KernelEventQueueCompatExports.KernelEventFilterAmpr);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], equeue);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], data);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x28..], extra);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], completionToken);
|
||||
|
||||
return AppendCommandBufferRecord(ctx, commandBuffer, record);
|
||||
}
|
||||
|
||||
private static bool AppendWriteAddressRecord(CpuContext ctx, ulong commandBuffer, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> record = stackalloc byte[(int)WriteAddressRecordSize];
|
||||
record.Clear();
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], WriteAddressRecordType);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], address);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], value);
|
||||
|
||||
return AppendCommandBufferRecord(ctx, commandBuffer, record);
|
||||
}
|
||||
@@ -604,9 +761,69 @@ public static class AmprExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CompleteKernelEventQueueRecord(CpuContext ctx, ulong recordAddress)
|
||||
{
|
||||
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
|
||||
if (!ctx.Memory.TryRead(recordAddress, record))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var filter = unchecked((short)BinaryPrimitives.ReadUInt32LittleEndian(record[0x04..]));
|
||||
var equeue = BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]);
|
||||
var ident = BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]);
|
||||
var userData = BinaryPrimitives.ReadUInt64LittleEndian(record[0x18..]);
|
||||
var data = BinaryPrimitives.ReadUInt64LittleEndian(record[0x20..]);
|
||||
var extra = BinaryPrimitives.ReadUInt64LittleEndian(record[0x28..]);
|
||||
|
||||
var queuedEvent = new KernelEventQueueCompatExports.KernelQueuedEvent(
|
||||
ident,
|
||||
filter,
|
||||
0x20,
|
||||
unchecked((uint)extra),
|
||||
data,
|
||||
userData);
|
||||
|
||||
_ = KernelEventQueueCompatExports.EnqueueEvent(equeue, queuedEvent);
|
||||
TraceAmpr(ctx, "complete_equeue", equeue, ident, data);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool CompleteWriteAddressRecord(CpuContext ctx, ulong recordAddress)
|
||||
{
|
||||
Span<byte> record = stackalloc byte[(int)WriteAddressRecordSize];
|
||||
if (!ctx.Memory.TryRead(recordAddress, record))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var address = BinaryPrimitives.ReadUInt64LittleEndian(record[0x08..]);
|
||||
var value = BinaryPrimitives.ReadUInt64LittleEndian(record[0x10..]);
|
||||
if (!ctx.TryWriteUInt64(address, value))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
TraceAmpr(ctx, "complete_write_address", address, value, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
|
||||
if (!_traceAmpr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -628,7 +845,7 @@ public static class AmprExports
|
||||
string? hostPath,
|
||||
int result)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
|
||||
if (!_traceAmprReads)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.Libs.AppContent;
|
||||
|
||||
@@ -11,6 +12,8 @@ public static class AppContentExports
|
||||
{
|
||||
private const ulong BootParamAttrOffset = 4;
|
||||
private const string Temp0MountPoint = "/temp0";
|
||||
private const uint AppParamSkuFlag = 0;
|
||||
private const int AppParamSkuFlagFull = 3;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "R9lA82OraNs",
|
||||
@@ -59,6 +62,41 @@ public static class AppContentExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "99b82IKXpH4",
|
||||
ExportName = "sceAppContentAppParamGetInt",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAppContent")]
|
||||
public static int AppContentAppParamGetInt(CpuContext ctx)
|
||||
{
|
||||
var paramId = (uint)ctx[CpuRegister.Rdi];
|
||||
var valueAddress = ctx[CpuRegister.Rsi];
|
||||
if (valueAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
int value;
|
||||
if (paramId == AppParamSkuFlag)
|
||||
{
|
||||
value = AppParamSkuFlagFull;
|
||||
}
|
||||
else if (!TryReadUserDefinedParam(paramId, out value))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||
if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceAppContent($"app_param_get_int id={paramId} value={value}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "buYbeLOGWmA",
|
||||
ExportName = "sceAppContentTemporaryDataMount2",
|
||||
@@ -83,6 +121,69 @@ public static class AppContentExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryReadUserDefinedParam(uint paramId, out int value)
|
||||
{
|
||||
value = 0;
|
||||
if (paramId is < 1 or > 4)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||
if (string.IsNullOrWhiteSpace(app0Root))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var paramJsonPath = Path.Combine(app0Root, "sce_sys", "param.json");
|
||||
if (!File.Exists(paramJsonPath))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = File.OpenRead(paramJsonPath);
|
||||
using var document = JsonDocument.Parse(stream);
|
||||
var propertyName = $"userDefinedParam{paramId}";
|
||||
if (document.RootElement.TryGetProperty(propertyName, out var element) &&
|
||||
element.TryGetInt32(out var parsedValue))
|
||||
{
|
||||
value = parsedValue;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
catch (JsonException)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
private static void TraceAppContent(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] app_content.{message}");
|
||||
}
|
||||
|
||||
private static string ResolveTemp0Root()
|
||||
{
|
||||
const string temp0VariableName = "SHARPEMU_TEMP0_DIR";
|
||||
|
||||
@@ -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
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Ampr;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
@@ -10,8 +11,13 @@ namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelAprCompatExports
|
||||
{
|
||||
private static readonly ConcurrentDictionary<uint, ulong> _submittedCommandBuffers = new();
|
||||
private static readonly ConcurrentDictionary<uint, AprSubmission> _submittedCommandBuffers = new();
|
||||
private static int _nextSubmissionId;
|
||||
private static int _aprWaitTraceCount;
|
||||
private static readonly bool _traceApr =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||
|
||||
private readonly record struct AprSubmission(ulong CommandBuffer, ulong Priority, ulong ResultAddress);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ASoW5WE-UPo",
|
||||
@@ -36,7 +42,13 @@ public static class KernelAprCompatExports
|
||||
submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
||||
}
|
||||
|
||||
_submittedCommandBuffers[submissionId] = commandBuffer;
|
||||
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, priority, resultAddress);
|
||||
|
||||
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
||||
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||
{
|
||||
@@ -60,20 +72,23 @@ public static class KernelAprCompatExports
|
||||
public static int KernelAprWaitCommandBuffer(CpuContext ctx)
|
||||
{
|
||||
var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var priority = ctx[CpuRegister.Rsi];
|
||||
var resultAddress = ctx[CpuRegister.Rdx];
|
||||
var waitArg1 = ctx[CpuRegister.Rsi];
|
||||
var waitArg2 = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!_submittedCommandBuffers.TryRemove(submissionId, out var commandBuffer))
|
||||
if (!_submittedCommandBuffers.TryRemove(submissionId, out var submission))
|
||||
{
|
||||
TraceAprWaitFailure(ctx, "wait_missing", submissionId, commandBuffer: 0, waitArg1, waitArg2);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress);
|
||||
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
|
||||
{
|
||||
TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
TraceApr(ctx, "wait", submissionId, commandBuffer, priority, resultAddress);
|
||||
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -91,7 +106,13 @@ public static class KernelAprCompatExports
|
||||
}
|
||||
|
||||
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
||||
_submittedCommandBuffers[submissionId] = commandBuffer;
|
||||
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
|
||||
|
||||
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
||||
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
TraceApr(ctx, "submit", submissionId, commandBuffer, ctx[CpuRegister.Rsi], 0);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -112,7 +133,13 @@ public static class KernelAprCompatExports
|
||||
}
|
||||
|
||||
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
||||
_submittedCommandBuffers[submissionId] = commandBuffer;
|
||||
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
|
||||
|
||||
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
||||
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||
{
|
||||
@@ -130,6 +157,27 @@ public static class KernelAprCompatExports
|
||||
return ctx.Memory.TryWrite(resultAddress, result);
|
||||
}
|
||||
|
||||
private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress)
|
||||
{
|
||||
if (waitArg2 == 0)
|
||||
{
|
||||
return submittedResultAddress;
|
||||
}
|
||||
|
||||
if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF)
|
||||
{
|
||||
return submittedResultAddress;
|
||||
}
|
||||
|
||||
return waitArg2;
|
||||
}
|
||||
|
||||
private static bool IsAmprCompletionToken(ulong value)
|
||||
{
|
||||
var tag = value >> 56;
|
||||
return tag is 0x0C or 0x10;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
@@ -145,7 +193,7 @@ public static class KernelAprCompatExports
|
||||
ulong priority,
|
||||
ulong aux)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
|
||||
if (!_traceApr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -154,5 +202,54 @@ public static class KernelAprCompatExports
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] apr.{operation}: id=0x{submissionId:X8} cmd=0x{commandBuffer:X16} priority=0x{priority:X16} aux=0x{aux:X16} ret=0x{returnRip:X16}");
|
||||
if (aux != 0 &&
|
||||
ctx.TryReadUInt64(aux, out var result0) &&
|
||||
ctx.TryReadUInt64(aux + sizeof(ulong), out var result1))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] apr.{operation}.result: addr=0x{aux:X16} q0=0x{result0:X16} q1=0x{result1:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void TraceAprWaitFailure(
|
||||
CpuContext ctx,
|
||||
string operation,
|
||||
uint submissionId,
|
||||
ulong commandBuffer,
|
||||
ulong priority,
|
||||
ulong resultAddress)
|
||||
{
|
||||
if (!_traceApr)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var traceCount = Interlocked.Increment(ref _aprWaitTraceCount);
|
||||
if (traceCount > 32 && (traceCount & 0x3FF) != 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var returnRip = 0UL;
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] apr.{operation}: id=0x{submissionId:X8} cmd=0x{commandBuffer:X16} " +
|
||||
$"rsi=0x{priority:X16} rdx=0x{resultAddress:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16} " +
|
||||
$"r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16} ret=0x{returnRip:X16}");
|
||||
TraceReadableQword(ctx, operation, "rsi", priority);
|
||||
TraceReadableQword(ctx, operation, "rdx", resultAddress);
|
||||
TraceReadableQword(ctx, operation, "rcx", ctx[CpuRegister.Rcx]);
|
||||
TraceReadableQword(ctx, operation, "r8", ctx[CpuRegister.R8]);
|
||||
}
|
||||
|
||||
private static void TraceReadableQword(CpuContext ctx, string operation, string name, ulong address)
|
||||
{
|
||||
if (address == 0 || !ctx.TryReadUInt64(address, out var value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] apr.{operation}.{name}: addr=0x{address:X16} q0=0x{value:X16}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -9,10 +9,30 @@ namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelEventQueueCompatExports
|
||||
{
|
||||
private const int KernelEventSize = 0x20;
|
||||
public const short KernelEventFilterGraphics = -14;
|
||||
public const short KernelEventFilterAmpr = -16;
|
||||
public const short KernelEventFilterAmprSystem = -17;
|
||||
|
||||
private static readonly object _eventQueueGate = new();
|
||||
private static readonly HashSet<ulong> _eventQueues = new();
|
||||
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
|
||||
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
|
||||
private static long _nextEventQueueHandle = 1;
|
||||
|
||||
public readonly record struct KernelQueuedEvent(
|
||||
ulong Ident,
|
||||
short Filter,
|
||||
ushort Flags,
|
||||
uint Fflags,
|
||||
ulong Data,
|
||||
ulong UserData);
|
||||
|
||||
private readonly record struct KernelEventRegistration(
|
||||
ulong Ident,
|
||||
short Filter,
|
||||
ulong UserData);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "D0OdFMjp46I",
|
||||
ExportName = "sceKernelCreateEqueue",
|
||||
@@ -30,6 +50,8 @@ public static class KernelEventQueueCompatExports
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
_eventQueues.Add(handle);
|
||||
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
|
||||
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||
@@ -52,6 +74,8 @@ public static class KernelEventQueueCompatExports
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
_eventQueues.Remove(handle);
|
||||
_pendingEvents.Remove(handle);
|
||||
_registeredEvents.Remove(handle);
|
||||
}
|
||||
|
||||
TraceEventQueue(ctx, "delete", handle);
|
||||
@@ -109,8 +133,16 @@ public static class KernelEventQueueCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelAddAmprEvent(CpuContext ctx)
|
||||
{
|
||||
TraceEventQueue(ctx, "add_ampr", ctx[CpuRegister.Rdi]);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var registered = RegisterEvent(
|
||||
handle,
|
||||
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||
KernelEventFilterAmpr,
|
||||
ctx[CpuRegister.Rdx]);
|
||||
TraceEventQueue(ctx, "add_ampr", handle);
|
||||
return registered
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -120,8 +152,16 @@ public static class KernelEventQueueCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelAddAmprSystemEvent(CpuContext ctx)
|
||||
{
|
||||
TraceEventQueue(ctx, "add_ampr_system", ctx[CpuRegister.Rdi]);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var registered = RegisterEvent(
|
||||
handle,
|
||||
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||
KernelEventFilterAmprSystem,
|
||||
ctx[CpuRegister.Rdx]);
|
||||
TraceEventQueue(ctx, "add_ampr_system", handle);
|
||||
return registered
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -131,8 +171,15 @@ public static class KernelEventQueueCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelDeleteAmprEvent(CpuContext ctx)
|
||||
{
|
||||
TraceEventQueue(ctx, "delete_ampr", ctx[CpuRegister.Rdi]);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var deleted = DeleteRegisteredEvent(
|
||||
handle,
|
||||
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||
KernelEventFilterAmpr);
|
||||
TraceEventQueue(ctx, "delete_ampr", handle);
|
||||
return deleted
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -142,8 +189,15 @@ public static class KernelEventQueueCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelDeleteAmprSystemEvent(CpuContext ctx)
|
||||
{
|
||||
TraceEventQueue(ctx, "delete_ampr_system", ctx[CpuRegister.Rdi]);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var deleted = DeleteRegisteredEvent(
|
||||
handle,
|
||||
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||
KernelEventFilterAmprSystem);
|
||||
TraceEventQueue(ctx, "delete_ampr_system", handle);
|
||||
return deleted
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -158,6 +212,57 @@ public static class KernelEventQueueCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "vz+pg2zdopI",
|
||||
ExportName = "sceKernelGetEventUserData",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelGetEventUserData(CpuContext ctx)
|
||||
{
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x18, out var userData);
|
||||
ctx[CpuRegister.Rax] = userData;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "mJ7aghmgvfc",
|
||||
ExportName = "sceKernelGetEventId",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelGetEventId(CpuContext ctx)
|
||||
{
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi], out var ident);
|
||||
ctx[CpuRegister.Rax] = ident;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "23CPPI1tyBY",
|
||||
ExportName = "sceKernelGetEventFilter",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelGetEventFilter(CpuContext ctx)
|
||||
{
|
||||
Span<byte> filterBytes = stackalloc byte[sizeof(short)];
|
||||
var filter = ctx.Memory.TryRead(ctx[CpuRegister.Rdi] + 0x08, filterBytes)
|
||||
? BinaryPrimitives.ReadInt16LittleEndian(filterBytes)
|
||||
: (short)0;
|
||||
ctx[CpuRegister.Rax] = unchecked((uint)filter);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "kwGyyjohI50",
|
||||
ExportName = "sceKernelGetEventData",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelGetEventData(CpuContext ctx)
|
||||
{
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x10, out var data);
|
||||
ctx[CpuRegister.Rax] = data;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fzyMKs9kim0",
|
||||
ExportName = "sceKernelWaitEqueue",
|
||||
@@ -165,23 +270,329 @@ public static class KernelEventQueueCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelWaitEqueue(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var eventsAddress = ctx[CpuRegister.Rsi];
|
||||
var eventCapacity = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
||||
var outCountAddress = ctx[CpuRegister.Rcx];
|
||||
var timeoutAddress = ctx[CpuRegister.R8];
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, 0))
|
||||
|
||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue"))
|
||||
if (deliveredCount > 0)
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-block", ctx[CpuRegister.Rdi]);
|
||||
TraceEventQueue(ctx, "wait-deliver", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
TraceEventQueue(ctx, "wait", ctx[CpuRegister.Rdi]);
|
||||
if (timeoutAddress == 0 &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEqueue",
|
||||
GetEventQueueWakeKey(handle),
|
||||
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress),
|
||||
() => HasPendingEvents(handle)))
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-block", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
TraceEventQueue(ctx, "wait", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
public static bool IsValidEqueue(ulong handle)
|
||||
{
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
return _eventQueues.Contains(handle);
|
||||
}
|
||||
}
|
||||
|
||||
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
|
||||
{
|
||||
var queued = false;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
if (!_eventQueues.Contains(handle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new LinkedList<KernelQueuedEvent>();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
queue.AddLast(queuedEvent);
|
||||
queued = true;
|
||||
}
|
||||
|
||||
if (queued)
|
||||
{
|
||||
WakeEventQueue(handle);
|
||||
}
|
||||
|
||||
return queued;
|
||||
}
|
||||
|
||||
public static bool RegisterEvent(
|
||||
ulong handle,
|
||||
ulong ident,
|
||||
short filter,
|
||||
ulong userData)
|
||||
{
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
if (!_eventQueues.Contains(handle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_registeredEvents.TryGetValue(handle, out var events))
|
||||
{
|
||||
events = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||
_registeredEvents[handle] = events;
|
||||
}
|
||||
|
||||
events[(ident, filter)] = new KernelEventRegistration(ident, filter, userData);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
public static bool DeleteRegisteredEvent(
|
||||
ulong handle,
|
||||
ulong ident,
|
||||
short filter)
|
||||
{
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
return _registeredEvents.TryGetValue(handle, out var events) &&
|
||||
events.Remove((ident, filter));
|
||||
}
|
||||
}
|
||||
|
||||
public static int TriggerRegisteredEvents(
|
||||
ulong ident,
|
||||
short filter,
|
||||
ulong data)
|
||||
{
|
||||
List<ulong>? wakeHandles = null;
|
||||
var triggeredCount = 0;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
foreach (var (handle, registrations) in _registeredEvents)
|
||||
{
|
||||
if (!registrations.TryGetValue((ident, filter), out var registration))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new LinkedList<KernelQueuedEvent>();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
QueueOrUpdateEvent(
|
||||
queue,
|
||||
new KernelQueuedEvent(
|
||||
registration.Ident,
|
||||
registration.Filter,
|
||||
0,
|
||||
1,
|
||||
data,
|
||||
registration.UserData));
|
||||
(wakeHandles ??= new List<ulong>()).Add(handle);
|
||||
triggeredCount++;
|
||||
}
|
||||
}
|
||||
|
||||
if (wakeHandles is not null)
|
||||
{
|
||||
foreach (var handle in wakeHandles)
|
||||
{
|
||||
WakeEventQueue(handle);
|
||||
}
|
||||
}
|
||||
|
||||
return triggeredCount;
|
||||
}
|
||||
|
||||
public static bool TriggerDisplayEvent(
|
||||
ulong handle,
|
||||
ulong ident,
|
||||
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)
|
||||
{
|
||||
if (eventsAddress == 0 || eventCapacity <= 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
KernelQueuedEvent[] events;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue) || queue.Count == 0)
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var count = Math.Min(eventCapacity, queue.Count);
|
||||
events = new KernelQueuedEvent[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
events[i] = queue.First!.Value;
|
||||
queue.RemoveFirst();
|
||||
}
|
||||
}
|
||||
|
||||
for (var i = 0; i < events.Length; i++)
|
||||
{
|
||||
if (!WriteKernelEvent(ctx, eventsAddress + ((ulong)i * KernelEventSize), events[i]))
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return events.Length;
|
||||
}
|
||||
|
||||
private static bool WriteKernelEvent(CpuContext ctx, ulong address, KernelQueuedEvent queuedEvent)
|
||||
{
|
||||
Span<byte> eventBytes = stackalloc byte[KernelEventSize];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x00..], queuedEvent.Ident);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(eventBytes[0x08..], queuedEvent.Filter);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(eventBytes[0x0A..], queuedEvent.Flags);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(eventBytes[0x0C..], queuedEvent.Fflags);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x10..], queuedEvent.Data);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(eventBytes[0x18..], queuedEvent.UserData);
|
||||
return ctx.Memory.TryWrite(address, eventBytes);
|
||||
}
|
||||
|
||||
private static void TraceEventQueue(CpuContext ctx, string operation, ulong handle)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EQUEUE"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -204,6 +204,16 @@ public static class KernelExports
|
||||
var nameAddress = ctx[CpuRegister.R8];
|
||||
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
|
||||
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))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -212,13 +222,22 @@ public static class KernelExports
|
||||
if (ShouldTracePthread())
|
||||
{
|
||||
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;
|
||||
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))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
@@ -252,6 +271,32 @@ public static class KernelExports
|
||||
return PthreadCreate(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3kg7rT0NQIs",
|
||||
ExportName = "scePthreadExit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadExit(CpuContext ctx)
|
||||
{
|
||||
var value = ctx[CpuRegister.Rdi];
|
||||
GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value);
|
||||
ctx[CpuRegister.Rax] = value;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FJrT5LuUBAU",
|
||||
ExportName = "pthread_exit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePosix")]
|
||||
public static int PosixPthreadExit(CpuContext ctx)
|
||||
{
|
||||
var value = ctx[CpuRegister.Rdi];
|
||||
GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value);
|
||||
ctx[CpuRegister.Rax] = value;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "onNY9Byn-W8",
|
||||
ExportName = "scePthreadJoin",
|
||||
@@ -261,10 +306,6 @@ public static class KernelExports
|
||||
{
|
||||
var threadId = ctx[CpuRegister.Rdi];
|
||||
var returnValueAddress = ctx[CpuRegister.Rsi];
|
||||
if (returnValueAddress != 0 && !ctx.TryWriteUInt64(returnValueAddress, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (ShouldTracePthread())
|
||||
{
|
||||
@@ -272,6 +313,30 @@ public static class KernelExports
|
||||
$"[LOADER][TRACE] pthread_join: thread=0x{threadId:X16} retval_out=0x{returnValueAddress:X16}");
|
||||
}
|
||||
|
||||
var returnValue = 0UL;
|
||||
if (GuestThreadExecution.Scheduler is { } scheduler &&
|
||||
!scheduler.TryJoinThread(ctx, threadId, out returnValue, out var error))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] pthread_join: thread=0x{threadId:X16}: {error}");
|
||||
var result = string.Equals(
|
||||
error,
|
||||
"thread cannot join itself",
|
||||
StringComparison.Ordinal)
|
||||
? OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
|
||||
: OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
if (returnValueAddress != 0 &&
|
||||
!ctx.TryWriteUInt64(returnValueAddress, returnValue))
|
||||
{
|
||||
ctx[CpuRegister.Rax] =
|
||||
unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,9 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
@@ -9,35 +11,48 @@ namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelPthreadCompatExports
|
||||
{
|
||||
private const int MutexTypeDefault = 1;
|
||||
private const int MutexTypeErrorCheck = 1;
|
||||
private const int MutexTypeRecursive = 2;
|
||||
private const int MutexTypeNormal = 3;
|
||||
private const int MutexTypeAdaptiveNp = 4;
|
||||
private const ulong StaticAdaptiveMutexInitializer = 1;
|
||||
private const ulong SyntheticMutexHandleBase = 0x00006000_0000_0000;
|
||||
private const ulong SyntheticMutexAttrHandleBase = 0x00006001_0000_0000;
|
||||
private const ulong SyntheticCondHandleBase = 0x00006002_0000_0000;
|
||||
private const int MutexObjectSize = 0x100;
|
||||
private const int MutexAttrObjectSize = 0x40;
|
||||
private const int CondObjectSize = 0x100;
|
||||
private const int DefaultSpuriousCondWakeMilliseconds = 1;
|
||||
private const int PthreadOnceUninitialized = 0;
|
||||
private const int PthreadOnceInProgress = 1;
|
||||
private const int PthreadOnceDone = 2;
|
||||
|
||||
private static readonly object _stateGate = new();
|
||||
private static readonly Dictionary<ulong, PthreadMutexState> _mutexStates = new();
|
||||
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
|
||||
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
|
||||
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
||||
private static readonly Dictionary<ulong, object> _onceGates = new();
|
||||
private static readonly HashSet<ulong> _condAttrStates = new();
|
||||
private static long _nextSyntheticMutexHandleId = 1;
|
||||
private static long _nextSyntheticMutexAttrHandleId = 1;
|
||||
private static long _nextSyntheticCondHandleId = 1;
|
||||
private static readonly bool _tracePthreads =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||
private static readonly bool _tracePthreadConds =
|
||||
_tracePthreads ||
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
|
||||
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
|
||||
|
||||
private sealed class PthreadMutexState
|
||||
{
|
||||
public SemaphoreSlim Semaphore { get; } = new(1, 1);
|
||||
public ulong OwnerThreadId { get; set; }
|
||||
public int RecursionCount { get; set; }
|
||||
public int Type { get; set; } = MutexTypeDefault;
|
||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||
public int Protocol { get; set; }
|
||||
}
|
||||
|
||||
private sealed class PthreadMutexWaiter
|
||||
{
|
||||
public required ulong ThreadId { get; init; }
|
||||
public int Reserved;
|
||||
}
|
||||
|
||||
private sealed class PthreadCondState
|
||||
{
|
||||
public object SyncRoot { get; } = new();
|
||||
@@ -60,6 +75,13 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "EotR8a3ASf4",
|
||||
ExportName = "pthread_self",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadSelf(CpuContext ctx) => PthreadSelf(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3PtV6p3QNX4",
|
||||
ExportName = "scePthreadEqual",
|
||||
@@ -343,6 +365,93 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "14bOACANTBo",
|
||||
ExportName = "scePthreadOnce",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadOnce(CpuContext ctx)
|
||||
{
|
||||
var onceAddress = ctx[CpuRegister.Rdi];
|
||||
var initRoutine = ctx[CpuRegister.Rsi];
|
||||
if (onceAddress == 0 || initRoutine == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, onceAddress, out var onceValue))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (onceValue == PthreadOnceDone)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
var gate = GetPthreadOnceGate(onceAddress);
|
||||
var shouldCall = false;
|
||||
lock (gate)
|
||||
{
|
||||
if (!TryReadInt32(ctx, onceAddress, out onceValue))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
while (onceValue == PthreadOnceInProgress)
|
||||
{
|
||||
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
|
||||
if (!TryReadInt32(ctx, onceAddress, out onceValue))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (onceValue != PthreadOnceDone)
|
||||
{
|
||||
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceInProgress))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
shouldCall = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (shouldCall)
|
||||
{
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
string? error = null;
|
||||
if (scheduler is null ||
|
||||
!scheduler.TryCallGuestFunction(ctx, initRoutine, 0, 0, 0, 0, "pthread_once", out error))
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
}
|
||||
|
||||
TracePthreadOnce(onceAddress, initRoutine, "failed", error);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
lock (gate)
|
||||
{
|
||||
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceDone))
|
||||
{
|
||||
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Monitor.PulseAll(gate);
|
||||
}
|
||||
}
|
||||
|
||||
TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||
{
|
||||
if (mutexAddress == 0)
|
||||
@@ -357,14 +466,28 @@ public static class KernelPthreadCompatExports
|
||||
Protocol = attr.Protocol,
|
||||
};
|
||||
|
||||
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexHandleBase, ref _nextSyntheticMutexHandleId);
|
||||
lock (_stateGate)
|
||||
if (!TryAllocateOpaqueObject(ctx, MutexObjectSize, out var handle))
|
||||
{
|
||||
_mutexStates[mutexAddress] = state;
|
||||
_mutexStates[syntheticHandle] = state;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -376,15 +499,10 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var resolvedAddress = ResolveMutexHandle(ctx, mutexAddress);
|
||||
PthreadMutexState? state;
|
||||
lock (_stateGate)
|
||||
_mutexStates.TryRemove(resolvedAddress, out var state);
|
||||
if (resolvedAddress != mutexAddress)
|
||||
{
|
||||
_mutexStates.TryGetValue(resolvedAddress, out state);
|
||||
_mutexStates.Remove(resolvedAddress);
|
||||
if (resolvedAddress != mutexAddress)
|
||||
{
|
||||
_mutexStates.Remove(mutexAddress);
|
||||
}
|
||||
_mutexStates.TryRemove(mutexAddress, out _);
|
||||
}
|
||||
|
||||
if (state is null)
|
||||
@@ -392,7 +510,7 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt64(mutexAddress, 0);
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, 0);
|
||||
state.Semaphore.Dispose();
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -447,15 +565,33 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
var acquired = true;
|
||||
if (tryOnly)
|
||||
var acquired = state.Semaphore.Wait(0);
|
||||
if (!acquired)
|
||||
{
|
||||
acquired = state.Semaphore.Wait(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
state.Semaphore.Wait();
|
||||
// Guest-thread blocking for pthread_mutex_lock is currently disabled.
|
||||
// Demon's Souls deadlocks during PS5SyncEvent initialization.
|
||||
/* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||
if (!tryOnly &&
|
||||
GuestThreadExecution.IsGuestThread &&
|
||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"pthread_mutex_lock",
|
||||
GetMutexWakeKey(resolvedAddress),
|
||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
} */
|
||||
|
||||
if (!tryOnly)
|
||||
{
|
||||
state.Semaphore.Wait();
|
||||
acquired = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (!acquired)
|
||||
{
|
||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
@@ -514,6 +650,7 @@ public static class KernelPthreadCompatExports
|
||||
try
|
||||
{
|
||||
state.Semaphore.Release();
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
|
||||
}
|
||||
catch (SemaphoreFullException)
|
||||
{
|
||||
@@ -533,14 +670,34 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var syntheticHandle = AllocateSyntheticHandle(SyntheticMutexAttrHandleBase, ref _nextSyntheticMutexAttrHandleId);
|
||||
lock (_stateGate)
|
||||
if (!TryAllocateOpaqueObject(ctx, MutexAttrObjectSize, out var handle))
|
||||
{
|
||||
_mutexAttrStates[attrAddress] = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||
_mutexAttrStates[syntheticHandle] = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var initialState = new PthreadMutexAttrState(MutexTypeErrorCheck, 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;
|
||||
}
|
||||
|
||||
@@ -572,21 +729,25 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
|
||||
PthreadMutexAttrState updatedState;
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
|
||||
{
|
||||
state = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||
state = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||
}
|
||||
|
||||
_mutexAttrStates[resolvedAddress] = state with { Type = NormalizeMutexType(type) };
|
||||
updatedState = state with { Type = NormalizeMutexType(type) };
|
||||
_mutexAttrStates[resolvedAddress] = updatedState;
|
||||
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)
|
||||
@@ -597,21 +758,25 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var resolvedAddress = ResolveMutexAttrHandle(ctx, attrAddress);
|
||||
PthreadMutexAttrState updatedState;
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (!_mutexAttrStates.TryGetValue(resolvedAddress, out var state))
|
||||
{
|
||||
state = new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||
state = new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||
}
|
||||
|
||||
_mutexAttrStates[resolvedAddress] = state with { Protocol = protocol };
|
||||
updatedState = state with { Protocol = protocol };
|
||||
_mutexAttrStates[resolvedAddress] = updatedState;
|
||||
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)
|
||||
@@ -621,22 +786,16 @@ public static class KernelPthreadCompatExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
if (_mutexStates.ContainsKey(mutexAddress))
|
||||
{
|
||||
if (_mutexStates.ContainsKey(mutexAddress))
|
||||
{
|
||||
return mutexAddress;
|
||||
}
|
||||
return mutexAddress;
|
||||
}
|
||||
|
||||
if (ctx.TryReadUInt64(mutexAddress, out var pointedHandle) && pointedHandle != 0)
|
||||
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle) && pointedHandle != 0)
|
||||
{
|
||||
lock (_stateGate)
|
||||
if (_mutexStates.ContainsKey(pointedHandle))
|
||||
{
|
||||
if (_mutexStates.ContainsKey(pointedHandle))
|
||||
{
|
||||
return pointedHandle;
|
||||
}
|
||||
return pointedHandle;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -652,16 +811,13 @@ public static class KernelPthreadCompatExports
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
||||
{
|
||||
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
||||
{
|
||||
resolvedAddress = mutexAddress;
|
||||
return true;
|
||||
}
|
||||
resolvedAddress = mutexAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(mutexAddress, out var pointedHandle))
|
||||
if (!KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var pointedHandle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -673,14 +829,11 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
if (pointedHandle != 0)
|
||||
{
|
||||
lock (_stateGate)
|
||||
if (_mutexStates.TryGetValue(pointedHandle, out state))
|
||||
{
|
||||
if (_mutexStates.TryGetValue(pointedHandle, out state))
|
||||
{
|
||||
_mutexStates[mutexAddress] = state;
|
||||
resolvedAddress = pointedHandle;
|
||||
return true;
|
||||
}
|
||||
_mutexStates.TryAdd(mutexAddress, state);
|
||||
resolvedAddress = pointedHandle;
|
||||
return true;
|
||||
}
|
||||
|
||||
resolvedAddress = pointedHandle;
|
||||
@@ -693,7 +846,7 @@ public static class KernelPthreadCompatExports
|
||||
return false;
|
||||
}
|
||||
|
||||
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeDefault, out resolvedAddress, out state);
|
||||
return CreateImplicitMutexState(ctx, mutexAddress, MutexTypeErrorCheck, out resolvedAddress, out state);
|
||||
}
|
||||
|
||||
private static ulong ResolveMutexAttrHandle(CpuContext ctx, ulong attrAddress)
|
||||
@@ -703,15 +856,7 @@ public static class KernelPthreadCompatExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (_mutexAttrStates.ContainsKey(attrAddress))
|
||||
{
|
||||
return attrAddress;
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.TryReadUInt64(attrAddress, out var pointedHandle) && pointedHandle != 0)
|
||||
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, attrAddress, out var pointedHandle) && pointedHandle != 0)
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
@@ -722,6 +867,14 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (_mutexAttrStates.ContainsKey(attrAddress))
|
||||
{
|
||||
return attrAddress;
|
||||
}
|
||||
}
|
||||
|
||||
return attrAddress;
|
||||
}
|
||||
|
||||
@@ -737,7 +890,7 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
return _mutexAttrStates.TryGetValue(resolvedAddress, out var state)
|
||||
? state
|
||||
: new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||
: new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,7 +909,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx.TryReadUInt64(condAddress, out var pointedHandle) && pointedHandle != 0)
|
||||
if (KernelMemoryCompatExports.TryReadUInt64Compat(ctx, condAddress, out var pointedHandle) && pointedHandle != 0)
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
@@ -788,7 +941,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
if (ctx is null || !ctx.TryReadUInt64(condAddress, out var pointedHandle))
|
||||
if (ctx is null || !KernelMemoryCompatExports.TryReadUInt64Compat(ctx, condAddress, out var pointedHandle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -816,23 +969,60 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
var createdState = new PthreadCondState();
|
||||
var syntheticHandle = AllocateSyntheticHandle(SyntheticCondHandleBase, ref _nextSyntheticCondHandleId);
|
||||
if (!TryAllocateOpaqueObject(ctx, CondObjectSize, out var handle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
_condStates[condAddress] = createdState;
|
||||
_condStates[syntheticHandle] = createdState;
|
||||
_condStates[handle] = createdState;
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt64(condAddress, syntheticHandle);
|
||||
resolvedAddress = syntheticHandle;
|
||||
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, handle))
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
_condStates.Remove(condAddress);
|
||||
_condStates.Remove(handle);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
resolvedAddress = handle;
|
||||
state = createdState;
|
||||
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));
|
||||
return baseAddress + (id << 4);
|
||||
address = 0;
|
||||
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)
|
||||
@@ -842,15 +1032,29 @@ public static class KernelPthreadCompatExports
|
||||
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)
|
||||
{
|
||||
var state = new PthreadCondState();
|
||||
_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;
|
||||
}
|
||||
|
||||
@@ -871,7 +1075,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt64(condAddress, 0);
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1007,6 +1211,63 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
|
||||
$"pthread_mutex:0x{resolvedMutexAddress:X16}";
|
||||
|
||||
private static bool TryReserveBlockedMutexLock(
|
||||
CpuContext ctx,
|
||||
ulong mutexAddress,
|
||||
ulong resolvedAddress,
|
||||
PthreadMutexState state,
|
||||
PthreadMutexWaiter waiter)
|
||||
{
|
||||
lock (state)
|
||||
{
|
||||
if (state.OwnerThreadId != 0 || state.RecursionCount != 0)
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-reserve-busy", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return false;
|
||||
}
|
||||
|
||||
state.OwnerThreadId = waiter.ThreadId;
|
||||
state.RecursionCount = 1;
|
||||
Interlocked.Exchange(ref waiter.Reserved, 1);
|
||||
}
|
||||
|
||||
TracePthreadMutex(ctx, "lock-reserve", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int CompleteBlockedMutexLock(
|
||||
CpuContext ctx,
|
||||
ulong mutexAddress,
|
||||
ulong resolvedAddress,
|
||||
PthreadMutexState state,
|
||||
PthreadMutexWaiter waiter)
|
||||
{
|
||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||
if (Interlocked.Exchange(ref waiter.Reserved, 0) == 1)
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-resume", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!state.Semaphore.Wait(0))
|
||||
{
|
||||
TracePthreadMutex(ctx, "lock-resume-busy", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
}
|
||||
|
||||
lock (state)
|
||||
{
|
||||
state.OwnerThreadId = currentThreadId;
|
||||
state.RecursionCount = 1;
|
||||
}
|
||||
|
||||
TracePthreadMutex(ctx, "lock-resume", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
|
||||
{
|
||||
if (timeoutUsec == 0)
|
||||
@@ -1031,15 +1292,55 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
return type switch
|
||||
{
|
||||
0 => MutexTypeDefault,
|
||||
0 => MutexTypeErrorCheck,
|
||||
1 => MutexTypeErrorCheck,
|
||||
2 => MutexTypeRecursive,
|
||||
3 => MutexTypeNormal,
|
||||
4 => MutexTypeAdaptiveNp,
|
||||
_ => MutexTypeDefault,
|
||||
_ => MutexTypeErrorCheck,
|
||||
};
|
||||
}
|
||||
|
||||
private static object GetPthreadOnceGate(ulong onceAddress)
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (!_onceGates.TryGetValue(onceAddress, out var gate))
|
||||
{
|
||||
gate = new object();
|
||||
_onceGates[onceAddress] = gate;
|
||||
}
|
||||
|
||||
return gate;
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
|
||||
{
|
||||
var createdState = new PthreadMutexState
|
||||
@@ -1047,7 +1348,19 @@ public static class KernelPthreadCompatExports
|
||||
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)
|
||||
{
|
||||
if (_mutexStates.TryGetValue(mutexAddress, out state))
|
||||
@@ -1056,18 +1369,27 @@ public static class KernelPthreadCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
if (_mutexStates.TryGetValue(syntheticHandle, out state))
|
||||
if (_mutexStates.TryGetValue(handle, out state))
|
||||
{
|
||||
resolvedAddress = syntheticHandle;
|
||||
resolvedAddress = handle;
|
||||
return true;
|
||||
}
|
||||
|
||||
_mutexStates[mutexAddress] = createdState;
|
||||
_mutexStates[syntheticHandle] = createdState;
|
||||
_mutexStates[handle] = createdState;
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt64(mutexAddress, syntheticHandle);
|
||||
resolvedAddress = syntheticHandle;
|
||||
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, mutexAddress, handle))
|
||||
{
|
||||
_mutexStates.TryRemove(mutexAddress, out _);
|
||||
_mutexStates.TryRemove(handle, out _);
|
||||
|
||||
resolvedAddress = 0;
|
||||
state = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
resolvedAddress = handle;
|
||||
state = createdState;
|
||||
return true;
|
||||
}
|
||||
@@ -1084,15 +1406,27 @@ public static class KernelPthreadCompatExports
|
||||
$"[LOADER][TRACE] pthread_self: stale_rdi=0x{ctx[CpuRegister.Rdi]:X16} thread=0x{currentThreadHandle:X16} tid=0x{currentThreadId:X16}");
|
||||
}
|
||||
|
||||
private static void TracePthreadMutex(CpuContext ctx, string operation, ulong mutexAddress, ulong resolvedAddress, PthreadMutexState? state, ulong currentThreadId, int result)
|
||||
private static void TracePthreadOnce(ulong onceAddress, ulong initRoutine, string operation, string? error)
|
||||
{
|
||||
if (!ShouldTracePthread())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = ctx.TryReadUInt64(mutexAddress, out var guestWord0);
|
||||
_ = ctx.TryReadUInt64(mutexAddress + 8, out var guestWord1);
|
||||
var suffix = string.IsNullOrWhiteSpace(error) ? string.Empty : $" error={error}";
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pthread_once_{operation}: once=0x{onceAddress:X16} init=0x{initRoutine:X16}{suffix}");
|
||||
}
|
||||
|
||||
private static void TracePthreadMutex(CpuContext ctx, string operation, ulong mutexAddress, ulong resolvedAddress, PthreadMutexState? state, ulong currentThreadId, int result)
|
||||
{
|
||||
if (!ShouldTracePthreadMutex(mutexAddress, resolvedAddress))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress, out var guestWord0);
|
||||
_ = KernelMemoryCompatExports.TryReadUInt64Compat(ctx, mutexAddress + 8, out var guestWord1);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
||||
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
|
||||
@@ -1102,7 +1436,7 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
|
||||
{
|
||||
if (!ShouldTracePthread())
|
||||
if (!_tracePthreadConds)
|
||||
{
|
||||
return;
|
||||
}
|
||||
@@ -1114,6 +1448,45 @@ public static class KernelPthreadCompatExports
|
||||
|
||||
private static bool ShouldTracePthread()
|
||||
{
|
||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||
return _tracePthreads;
|
||||
}
|
||||
|
||||
private static bool ShouldTracePthreadMutex(ulong mutexAddress, ulong resolvedAddress)
|
||||
{
|
||||
if (_tracePthreadMutexFilter is null || _tracePthreadMutexFilter.Count == 0)
|
||||
{
|
||||
return _tracePthreads;
|
||||
}
|
||||
|
||||
return _tracePthreadMutexFilter.Contains(mutexAddress) ||
|
||||
_tracePthreadMutexFilter.Contains(resolvedAddress);
|
||||
}
|
||||
|
||||
private static HashSet<ulong>? ParseTraceAddressFilter(string? filter)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(filter))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var addresses = new HashSet<ulong>();
|
||||
foreach (var token in filter.Split(new[] { ',', ';', ' ' }, StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries))
|
||||
{
|
||||
var normalized = token.StartsWith("0x", StringComparison.OrdinalIgnoreCase)
|
||||
? token[2..]
|
||||
: token;
|
||||
normalized = normalized.TrimStart('0');
|
||||
|
||||
if (ulong.TryParse(
|
||||
normalized.Length == 0 ? "0" : normalized,
|
||||
System.Globalization.NumberStyles.HexNumber,
|
||||
System.Globalization.CultureInfo.InvariantCulture,
|
||||
out var address))
|
||||
{
|
||||
addresses.Add(address);
|
||||
}
|
||||
}
|
||||
|
||||
return addresses.Count == 0 ? null : addresses;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
@@ -12,13 +13,13 @@ namespace SharpEmu.Libs.Kernel;
|
||||
public static class KernelPthreadExtendedCompatExports
|
||||
{
|
||||
private const int DefaultThreadPriority = 700;
|
||||
private const ulong DefaultThreadAffinityMask = ulong.MaxValue;
|
||||
private const ulong DefaultThreadAffinityMask = 0x7FUL;
|
||||
private const int DefaultDetachState = 0;
|
||||
private const ulong DefaultGuardSize = 0x1000UL;
|
||||
private const ulong DefaultStackSize = 0x1_00000UL;
|
||||
private const int DefaultInheritSched = 0;
|
||||
private const int DefaultSchedPolicy = 0;
|
||||
private const int DefaultSchedPriority = 0;
|
||||
private const int DefaultInheritSched = 4;
|
||||
private const int DefaultSchedPolicy = 1;
|
||||
private const int DefaultSchedPriority = DefaultThreadPriority;
|
||||
private const ulong SyntheticRwlockHandleBase = 0x00006003_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, PthreadAttrState> _attrStates = new();
|
||||
private static readonly Dictionary<ulong, PthreadRwlockState> _rwlockStates = new();
|
||||
private static readonly Dictionary<int, TlsKeyState> _tlsKeys = new();
|
||||
private static readonly ConcurrentDictionary<int, TlsKeyState> _tlsKeys = new();
|
||||
private static int _nextTlsKey = 1;
|
||||
private static long _nextSyntheticRwlockHandleId = 1;
|
||||
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
||||
|
||||
private static readonly Dictionary<ulong, Dictionary<int, ulong>> _threadLocalSpecific = new();
|
||||
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
||||
|
||||
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
|
||||
{
|
||||
@@ -185,6 +228,35 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rcrVFJsQWRY",
|
||||
ExportName = "scePthreadGetaffinity",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadGetaffinity(CpuContext ctx)
|
||||
{
|
||||
var thread = ctx[CpuRegister.Rdi];
|
||||
var outMaskAddress = ctx[CpuRegister.Rsi];
|
||||
if (thread == 0 || outMaskAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
ulong affinityMask;
|
||||
lock (_stateGate)
|
||||
{
|
||||
affinityMask = GetOrCreateThreadStateLocked(thread).AffinityMask;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(outMaskAddress, affinityMask))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1tKyG7RlMJo",
|
||||
ExportName = "scePthreadGetprio",
|
||||
@@ -489,6 +561,37 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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(
|
||||
Nid = "-fA+7ZlGDQs",
|
||||
ExportName = "scePthreadAttrGetstacksize",
|
||||
@@ -733,7 +836,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
_rwlockStates[syntheticHandle] = rwlock;
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle);
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -787,7 +890,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt64(rwlockAddress, 0);
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, 0);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -899,15 +1002,13 @@ public static class KernelPthreadExtendedCompatExports
|
||||
}
|
||||
|
||||
int key;
|
||||
lock (_stateGate)
|
||||
while (true)
|
||||
{
|
||||
while (_tlsKeys.ContainsKey(_nextTlsKey))
|
||||
key = Interlocked.Increment(ref _nextTlsKey) - 1;
|
||||
if (_tlsKeys.TryAdd(key, new TlsKeyState(destructor)))
|
||||
{
|
||||
_nextTlsKey++;
|
||||
break;
|
||||
}
|
||||
|
||||
key = _nextTlsKey++;
|
||||
_tlsKeys[key] = new TlsKeyState(destructor);
|
||||
}
|
||||
|
||||
if (!TryWriteInt32(ctx, outKeyAddress, key))
|
||||
@@ -934,17 +1035,14 @@ public static class KernelPthreadExtendedCompatExports
|
||||
public static int PosixPthreadKeyDelete(CpuContext ctx)
|
||||
{
|
||||
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
lock (_stateGate)
|
||||
if (!_tlsKeys.TryRemove(key, out _))
|
||||
{
|
||||
if (!_tlsKeys.Remove(key))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
foreach (var entry in _threadLocalSpecific)
|
||||
{
|
||||
entry.Value.Remove(key);
|
||||
}
|
||||
foreach (var values in _threadLocalSpecific.Values)
|
||||
{
|
||||
values.TryRemove(key, out _);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -968,22 +1066,15 @@ public static class KernelPthreadExtendedCompatExports
|
||||
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var value = ctx[CpuRegister.Rsi];
|
||||
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
||||
lock (_stateGate)
|
||||
if (!_tlsKeys.ContainsKey(key))
|
||||
{
|
||||
if (!_tlsKeys.TryGetValue(key, out _))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (!_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values))
|
||||
{
|
||||
values = new Dictionary<int, ulong>();
|
||||
_threadLocalSpecific[currentThreadHandle] = values;
|
||||
}
|
||||
|
||||
values[key] = value;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
var values = _threadLocalSpecific.GetOrAdd(
|
||||
currentThreadHandle,
|
||||
static _ => new ConcurrentDictionary<int, ulong>());
|
||||
values[key] = value;
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -1005,19 +1096,16 @@ public static class KernelPthreadExtendedCompatExports
|
||||
var key = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var currentThreadHandle = KernelPthreadState.GetCurrentThreadHandle();
|
||||
ulong value = 0;
|
||||
lock (_stateGate)
|
||||
if (!_tlsKeys.ContainsKey(key))
|
||||
{
|
||||
if (!_tlsKeys.TryGetValue(key, out _))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) &&
|
||||
values.TryGetValue(key, out var storedValue))
|
||||
{
|
||||
value = storedValue;
|
||||
}
|
||||
if (_threadLocalSpecific.TryGetValue(currentThreadHandle, out var values) &&
|
||||
values.TryGetValue(key, out var storedValue))
|
||||
{
|
||||
value = storedValue;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = value;
|
||||
@@ -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)
|
||||
{
|
||||
@@ -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;
|
||||
}
|
||||
@@ -1170,7 +1258,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
_rwlockStates[syntheticHandle] = createdRwlock;
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle);
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle);
|
||||
resolvedAddress = syntheticHandle;
|
||||
rwlock = createdRwlock;
|
||||
return true;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
@@ -11,8 +12,7 @@ internal static class KernelPthreadState
|
||||
{
|
||||
private const int ThreadObjectSize = 0x1000;
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static readonly Dictionary<ulong, ThreadIdentity> Threads = new();
|
||||
private static readonly ConcurrentDictionary<ulong, ThreadIdentity> Threads = new();
|
||||
private static readonly byte[] ZeroThreadObject = new byte[ThreadObjectSize];
|
||||
private static long _nextUniqueThreadId = 1;
|
||||
|
||||
@@ -56,10 +56,7 @@ internal static class KernelPthreadState
|
||||
|
||||
internal static bool TryGetThreadIdentity(ulong threadHandle, out ThreadIdentity identity)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
return Threads.TryGetValue(threadHandle, out identity);
|
||||
}
|
||||
return Threads.TryGetValue(threadHandle, out identity);
|
||||
}
|
||||
|
||||
private static void EnsureCurrentThreadRegistered()
|
||||
@@ -81,10 +78,7 @@ internal static class KernelPthreadState
|
||||
Marshal.Copy(ZeroThreadObject, 0, pointer, ThreadObjectSize);
|
||||
|
||||
var handle = unchecked((ulong)pointer.ToInt64());
|
||||
lock (Gate)
|
||||
{
|
||||
Threads[handle] = new ThreadIdentity(uniqueId, string.IsNullOrWhiteSpace(name) ? $"Thread-{uniqueId:X}" : name);
|
||||
}
|
||||
Threads[handle] = new ThreadIdentity(uniqueId, string.IsNullOrWhiteSpace(name) ? $"Thread-{uniqueId:X}" : name);
|
||||
|
||||
return handle;
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Fiber;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
@@ -25,12 +26,15 @@ public static class KernelRuntimeCompatExports
|
||||
private const ulong TlsNewReplaceOffset = 0x300;
|
||||
private const int MallocReplaceSize = 0x70;
|
||||
private const int NewReplaceSize = 0x68;
|
||||
private const int OrbisTimesecSize = sizeof(long) + sizeof(uint) + sizeof(uint);
|
||||
private const ulong ModuleInfoHandleOffset = 0x108;
|
||||
private const ulong ModuleInfoNameOffset = 0x10;
|
||||
private const int ModuleInfoNameMaxBytes = 64;
|
||||
private const ulong DefaultKernelTscFrequency = 10_000_000UL;
|
||||
private const ulong PrtAreaStartAddress = 0x0000001000000000UL;
|
||||
private const ulong PrtAreaSize = 0x000000EC00000000UL;
|
||||
private const int MapFlagFixed = 0x10;
|
||||
private const ulong DefaultVirtualRangeAlignment = 0x4000UL;
|
||||
private const int AioInitParamSize = 0x3C;
|
||||
private const uint MemCommit = 0x1000;
|
||||
private const uint MemReserve = 0x2000;
|
||||
@@ -52,6 +56,14 @@ public static class KernelRuntimeCompatExports
|
||||
private static readonly object _prtApertureGate = new();
|
||||
private static readonly (ulong Base, ulong Size)[] _prtApertures = new (ulong Base, ulong Size)[3];
|
||||
private static int _stackChkFailCount;
|
||||
private static long _usleepTraceCount;
|
||||
private static readonly bool _traceUsleep =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal);
|
||||
private static readonly bool _traceGuestThreads =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_GUEST_THREADS"), "1", StringComparison.Ordinal);
|
||||
|
||||
[ThreadStatic]
|
||||
private static int _shortUsleepCount;
|
||||
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate ulong RdtscDelegate();
|
||||
@@ -64,33 +76,104 @@ public static class KernelRuntimeCompatExports
|
||||
public static int KernelUsleep(CpuContext ctx)
|
||||
{
|
||||
var micros = ctx[CpuRegister.Rdi];
|
||||
TraceUsleepSpin(ctx, micros);
|
||||
if (micros == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var sleepMilliseconds = (int)Math.Min(micros / 1000UL, int.MaxValue);
|
||||
if (sleepMilliseconds > 0)
|
||||
{
|
||||
Thread.Sleep(sleepMilliseconds);
|
||||
}
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelUsleep");
|
||||
|
||||
var remainingMicros = micros % 1000UL;
|
||||
if (remainingMicros > 0)
|
||||
if (micros < 1000)
|
||||
{
|
||||
var targetTicks = (long)((double)remainingMicros * Stopwatch.Frequency / 1_000_000.0);
|
||||
var spin = Stopwatch.StartNew();
|
||||
while (spin.ElapsedTicks < targetTicks)
|
||||
// Guest worker pools use usleep(1) as a polling backoff. Periodically
|
||||
// relinquish a full host time slice so spin workers cannot starve producers.
|
||||
if ((++_shortUsleepCount & 31) == 0)
|
||||
{
|
||||
Thread.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;
|
||||
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(
|
||||
Nid = "QBi7HCK03hw",
|
||||
ExportName = "sceKernelClockGettime",
|
||||
@@ -156,6 +239,37 @@ public static class KernelRuntimeCompatExports
|
||||
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(
|
||||
Nid = "-2IRUCO--PM",
|
||||
ExportName = "sceKernelReadTsc",
|
||||
@@ -497,19 +611,16 @@ public static class KernelRuntimeCompatExports
|
||||
public static int ErrorAddress(CpuContext ctx)
|
||||
{
|
||||
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
||||
if (address != 0)
|
||||
{
|
||||
Span<byte> zero = stackalloc byte[sizeof(int)];
|
||||
if (!ctx.Memory.TryWrite(address, zero))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = address;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
internal static bool TrySetErrno(CpuContext ctx, int value)
|
||||
{
|
||||
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
||||
return address != 0 && TryWriteInt32(ctx, address, value);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bnZxYgAFeA0",
|
||||
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
|
||||
@@ -552,6 +663,17 @@ public static class KernelRuntimeCompatExports
|
||||
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(
|
||||
Nid = "ca7v6Cxulzs",
|
||||
ExportName = "sceKernelSetGPO",
|
||||
@@ -590,7 +712,7 @@ public static class KernelRuntimeCompatExports
|
||||
{
|
||||
var inOutAddressPointer = ctx[CpuRegister.Rdi];
|
||||
var length = ctx[CpuRegister.Rsi];
|
||||
var _flags = ctx[CpuRegister.Rdx];
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var alignment = ctx[CpuRegister.Rcx];
|
||||
if (inOutAddressPointer == 0 || length == 0)
|
||||
{
|
||||
@@ -602,7 +724,8 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var effectiveAlignment = alignment == 0 ? 0x10000UL : alignment;
|
||||
var effectiveAlignment = alignment == 0 ? DefaultVirtualRangeAlignment : alignment;
|
||||
var fixedMapping = (flags & MapFlagFixed) != 0;
|
||||
ulong desiredAddress;
|
||||
lock (_stateGate)
|
||||
{
|
||||
@@ -611,13 +734,13 @@ public static class KernelRuntimeCompatExports
|
||||
: AlignUp(_nextReservedVirtualBase, effectiveAlignment);
|
||||
}
|
||||
|
||||
if (!TryReserveVirtualRange(ctx, desiredAddress, length, out var mappedAddress))
|
||||
if (!TryReserveVirtualRange(ctx, desiredAddress, length, effectiveAlignment, allowSearch: !fixedMapping, out var mappedAddress))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16}");
|
||||
$"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16} flags=0x{flags:X8} align=0x{effectiveAlignment:X16}");
|
||||
|
||||
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
||||
{
|
||||
@@ -901,14 +1024,12 @@ public static class KernelRuntimeCompatExports
|
||||
public static int StackCheckFail(CpuContext ctx)
|
||||
{
|
||||
var count = Interlocked.Increment(ref _stackChkFailCount);
|
||||
if (count <= 8)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARNING] __stack_chk_fail recovery#{count}: rip=0x{ctx.Rip:X16} rdi=0x{ctx[CpuRegister.Rdi]:X16}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] __stack_chk_fail#{count}: rip=0x{ctx.Rip:X16} rdi=0x{ctx[CpuRegister.Rdi]:X16}");
|
||||
var result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP;
|
||||
GuestThreadExecution.RequestCurrentEntryExit("__stack_chk_fail", result);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -1024,6 +1145,111 @@ public static class KernelRuntimeCompatExports
|
||||
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(
|
||||
Nid = "vYU8P9Td2Zo",
|
||||
ExportName = "sceKernelAioInitializeImpl",
|
||||
@@ -1306,30 +1532,40 @@ public static class KernelRuntimeCompatExports
|
||||
ulong.TryParse(overrideHzText, out var overrideHz) &&
|
||||
overrideHz >= minSane)
|
||||
{
|
||||
TraceKernelTscFrequency("env", overrideHz);
|
||||
return overrideHz;
|
||||
}
|
||||
|
||||
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
|
||||
{
|
||||
return cpuidHz;
|
||||
}
|
||||
|
||||
if (TryCalibrateHostTscFrequency(out ulong calibratedHz) && calibratedHz >= minSane)
|
||||
{
|
||||
TraceKernelTscFrequency("calibrated-rdtsc", calibratedHz);
|
||||
return calibratedHz;
|
||||
}
|
||||
|
||||
if (TryResolveCpuidTscFrequency(out ulong cpuidHz) && cpuidHz >= minSane)
|
||||
{
|
||||
TraceKernelTscFrequency("cpuid", cpuidHz);
|
||||
return cpuidHz;
|
||||
}
|
||||
|
||||
var hostQpc = Stopwatch.Frequency > 0
|
||||
? unchecked((ulong)Stopwatch.Frequency)
|
||||
: DefaultKernelTscFrequency;
|
||||
if (hostQpc >= minSane)
|
||||
{
|
||||
TraceKernelTscFrequency("qpc", hostQpc);
|
||||
return hostQpc;
|
||||
}
|
||||
|
||||
TraceKernelTscFrequency("default", DefaultKernelTscFrequency);
|
||||
return DefaultKernelTscFrequency;
|
||||
}
|
||||
|
||||
private static void TraceKernelTscFrequency(string source, ulong frequencyHz)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Kernel TSC frequency: {frequencyHz} Hz ({source})");
|
||||
}
|
||||
|
||||
private static bool TryResolveCpuidTscFrequency(out ulong frequencyHz)
|
||||
{
|
||||
frequencyHz = 0;
|
||||
@@ -1488,7 +1724,13 @@ public static class KernelRuntimeCompatExports
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||
|
||||
private static bool TryReserveVirtualRange(CpuContext ctx, ulong desiredAddress, ulong length, out ulong mappedAddress)
|
||||
private static bool TryReserveVirtualRange(
|
||||
CpuContext ctx,
|
||||
ulong desiredAddress,
|
||||
ulong length,
|
||||
ulong alignment,
|
||||
bool allowSearch,
|
||||
out ulong mappedAddress)
|
||||
{
|
||||
mappedAddress = 0;
|
||||
if (length == 0)
|
||||
@@ -1500,40 +1742,51 @@ public static class KernelRuntimeCompatExports
|
||||
{
|
||||
object memoryObject = ctx.Memory;
|
||||
MethodInfo? allocateAt = null;
|
||||
MethodInfo? allocateAtOrAbove = null;
|
||||
var allocateAtHasAllowAlternativeArg = false;
|
||||
for (var depth = 0; depth < 4; depth++)
|
||||
{
|
||||
foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
if (!string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var parameters = candidate.GetParameters();
|
||||
if (parameters.Length == 3 &&
|
||||
parameters[0].ParameterType == typeof(ulong) &&
|
||||
parameters[1].ParameterType == typeof(ulong) &&
|
||||
parameters[2].ParameterType == typeof(bool))
|
||||
{
|
||||
allocateAt = candidate;
|
||||
allocateAtHasAllowAlternativeArg = false;
|
||||
break;
|
||||
}
|
||||
|
||||
if (parameters.Length == 4 &&
|
||||
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
|
||||
parameters.Length == 5 &&
|
||||
parameters[0].ParameterType == typeof(ulong) &&
|
||||
parameters[1].ParameterType == typeof(ulong) &&
|
||||
parameters[2].ParameterType == typeof(bool) &&
|
||||
parameters[3].ParameterType == typeof(bool))
|
||||
parameters[3].ParameterType == typeof(ulong) &&
|
||||
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
|
||||
{
|
||||
allocateAtOrAbove = candidate;
|
||||
}
|
||||
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
||||
{
|
||||
if (parameters.Length == 3 &&
|
||||
parameters[0].ParameterType == typeof(ulong) &&
|
||||
parameters[1].ParameterType == typeof(ulong) &&
|
||||
parameters[2].ParameterType == typeof(bool))
|
||||
{
|
||||
allocateAt = candidate;
|
||||
allocateAtHasAllowAlternativeArg = false;
|
||||
}
|
||||
else if (parameters.Length == 4 &&
|
||||
parameters[0].ParameterType == typeof(ulong) &&
|
||||
parameters[1].ParameterType == typeof(ulong) &&
|
||||
parameters[2].ParameterType == typeof(bool) &&
|
||||
parameters[3].ParameterType == typeof(bool))
|
||||
{
|
||||
allocateAt = candidate;
|
||||
allocateAtHasAllowAlternativeArg = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocateAtOrAbove is not null && allocateAt is not null)
|
||||
{
|
||||
allocateAt = candidate;
|
||||
allocateAtHasAllowAlternativeArg = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocateAt is not null)
|
||||
if (allocateAtOrAbove is not null || allocateAt is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
@@ -1553,6 +1806,18 @@ public static class KernelRuntimeCompatExports
|
||||
memoryObject = innerValue;
|
||||
}
|
||||
|
||||
if (allowSearch && allocateAtOrAbove is not null)
|
||||
{
|
||||
var searchArgs = new object[] { desiredAddress, length, false, alignment, 0UL };
|
||||
var searchResult = allocateAtOrAbove.Invoke(memoryObject, searchArgs);
|
||||
if (searchResult is bool trueValue && trueValue &&
|
||||
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
||||
{
|
||||
mappedAddress = searchedAddress;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocateAt is null)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
||||
@@ -1560,7 +1825,7 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
|
||||
var invokeArgs = allocateAtHasAllowAlternativeArg
|
||||
? new object[] { desiredAddress, length, false, true }
|
||||
? new object[] { desiredAddress, length, false, allowSearch }
|
||||
: new object[] { desiredAddress, length, false };
|
||||
var result = allocateAt.Invoke(memoryObject, invokeArgs);
|
||||
if (result is not ulong allocated || allocated == 0)
|
||||
|
||||
@@ -0,0 +1,318 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
private const int MaxSemaphoreNameLength = 128;
|
||||
private static readonly ConcurrentDictionary<uint, KernelSemaphoreState> _semaphores = new();
|
||||
private static int _nextSemaphoreHandle = 1;
|
||||
|
||||
private sealed class KernelSemaphoreState
|
||||
{
|
||||
public required string Name { get; init; }
|
||||
public required int InitialCount { get; init; }
|
||||
public required int MaxCount { get; init; }
|
||||
public int Count { get; set; }
|
||||
public int WaitingThreads { get; set; }
|
||||
public object Gate { get; } = new();
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "188x57JYp0g",
|
||||
ExportName = "sceKernelCreateSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelCreateSema(CpuContext ctx)
|
||||
{
|
||||
var semaphoreAddress = ctx[CpuRegister.Rdi];
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
var attr = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||
var initialCount = unchecked((int)ctx[CpuRegister.Rcx]);
|
||||
var maxCount = unchecked((int)ctx[CpuRegister.R8]);
|
||||
var optionAddress = ctx[CpuRegister.R9];
|
||||
|
||||
if (semaphoreAddress == 0 ||
|
||||
nameAddress == 0 ||
|
||||
attr > 2 ||
|
||||
initialCount < 0 ||
|
||||
maxCount <= 0 ||
|
||||
initialCount > maxCount ||
|
||||
optionAddress != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||
if (handle == 0)
|
||||
{
|
||||
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||
}
|
||||
|
||||
_semaphores[handle] = new KernelSemaphoreState
|
||||
{
|
||||
Name = name,
|
||||
InitialCount = initialCount,
|
||||
MaxCount = maxCount,
|
||||
Count = initialCount,
|
||||
};
|
||||
|
||||
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||
{
|
||||
_semaphores.TryRemove(handle, out _);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Zxa0VhQVTsk",
|
||||
ExportName = "sceKernelWaitSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelWaitSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var timeoutAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count >= needCount)
|
||||
{
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
if (!TryReadUInt32(ctx, timeoutAddress, out _))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "12wOHk8ywb0",
|
||||
ExportName = "sceKernelPollSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelPollSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var needCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count < needCount)
|
||||
{
|
||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
}
|
||||
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4czppHBiriw",
|
||||
ExportName = "sceKernelSignalSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelSignalSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var signalCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (signalCount <= 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count > semaphore.MaxCount - signalCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
semaphore.Count += signalCount;
|
||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4DM06U2BNEY",
|
||||
ExportName = "sceKernelCancelSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelCancelSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var setCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var waitingThreadsAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (setCount > semaphore.MaxCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
||||
semaphore.WaitingThreads = 0;
|
||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "R1Jvn8bSCW8",
|
||||
ExportName = "sceKernelDeleteSema",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelDeleteSema(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
if (!_semaphores.TryRemove(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
var value = (int)result;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (address == 0 || maxLength <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[Math.Min(maxLength, 4096)];
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address + (ulong)i, current))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, i);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes[i] = current[0];
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceSemaphore(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] sema.{message}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,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,76 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Np;
|
||||
|
||||
public static class NpEntitlementAccessExports
|
||||
{
|
||||
private const int BootParamClearSize = 0x20;
|
||||
private const int EmptyAddcontInfoListSize = 0x10;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jO8DM8oyego",
|
||||
ExportName = "sceNpEntitlementAccessInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpEntitlementAccess")]
|
||||
public static int NpEntitlementAccessInitialize(CpuContext ctx)
|
||||
{
|
||||
var initParam = ctx[CpuRegister.Rdi];
|
||||
var bootParam = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (bootParam != 0)
|
||||
{
|
||||
Span<byte> clear = stackalloc byte[BootParamClearSize];
|
||||
clear.Clear();
|
||||
if (!ctx.Memory.TryWrite(bootParam, clear))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TFyU+KFBv54",
|
||||
ExportName = "sceNpEntitlementAccessGetAddcontEntitlementInfoList",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpEntitlementAccess")]
|
||||
public static int NpEntitlementAccessGetAddcontEntitlementInfoList(CpuContext ctx)
|
||||
{
|
||||
var listAddress = ctx[CpuRegister.Rsi];
|
||||
if (listAddress != 0)
|
||||
{
|
||||
Span<byte> emptyList = stackalloc byte[EmptyAddcontInfoListSize];
|
||||
emptyList.Clear();
|
||||
if (!ctx.Memory.TryWrite(listAddress, emptyList))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
TraceNpEntitlementAccess(
|
||||
$"get_addcont_info_list service=0x{ctx[CpuRegister.Rdi]:X16} list=0x{listAddress:X16} " +
|
||||
$"max={ctx[CpuRegister.Rdx]} flags=0x{ctx[CpuRegister.Rcx]:X16} -> empty");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
private static void TraceNpEntitlementAccess(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_NP"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] np.entitlement.{message}");
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,188 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
public static class PadExports
|
||||
{
|
||||
private const int OrbisPadErrorInvalidHandle = unchecked((int)0x80920003);
|
||||
private const int OrbisPadErrorNotInitialized = unchecked((int)0x80920005);
|
||||
private const int OrbisPadErrorDeviceNotConnected = unchecked((int)0x80920007);
|
||||
private const int OrbisPadErrorDeviceNoHandle = unchecked((int)0x80920008);
|
||||
private const int PrimaryUserId = 1;
|
||||
private const int StandardPortType = 0;
|
||||
private const int PrimaryPadHandle = 1;
|
||||
private const int ControllerInformationSize = 0x1C;
|
||||
private const int PadDataSize = 0x78;
|
||||
|
||||
private static bool _initialized;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hv1luiJrqQM",
|
||||
ExportName = "scePadInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadInit(CpuContext ctx)
|
||||
{
|
||||
_initialized = true;
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xk0AcarP3V4",
|
||||
ExportName = "scePadOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadOpen(CpuContext ctx)
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var index = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var parameterAddress = ctx[CpuRegister.Rcx];
|
||||
if (!_initialized)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorNotInitialized);
|
||||
}
|
||||
|
||||
if (userId == -1)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
|
||||
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorDeviceNotConnected);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, PrimaryPadHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "clVvL4ZDntw",
|
||||
ExportName = "scePadSetMotionSensorState",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadSetMotionSensorState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return handle == PrimaryPadHandle
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "gjP9-KQzoUk",
|
||||
ExportName = "scePadGetControllerInformation",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadGetControllerInformation(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (informationAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> information = stackalloc byte[ControllerInformationSize];
|
||||
BinaryPrimitives.WriteSingleLittleEndian(information[0x00..], 44.86f);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(information[0x04..], 1920);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(information[0x06..], 943);
|
||||
information[0x08] = 30;
|
||||
information[0x09] = 30;
|
||||
information[0x0A] = StandardPortType;
|
||||
information[0x0B] = 1;
|
||||
information[0x0C] = 1;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
|
||||
|
||||
return ctx.Memory.TryWrite(informationAddress, information)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "YndgXqQVV7c",
|
||||
ExportName = "scePadReadState",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadReadState(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (dataAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
return WriteNeutralPadData(ctx, dataAddress)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "q1cHNfGycLI",
|
||||
ExportName = "scePadRead",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadRead(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (dataAddress == 0 || count < 1 || count > 64)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
return WriteNeutralPadData(ctx, dataAddress)
|
||||
? SetReturn(ctx, 1)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[PadDataSize];
|
||||
data.Clear();
|
||||
data[0x04] = 128;
|
||||
data[0x05] = 128;
|
||||
data[0x06] = 128;
|
||||
data[0x07] = 128;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||
data[0x4C] = 1;
|
||||
var timestampTicks = Stopwatch.GetTimestamp();
|
||||
var timestampMicroseconds =
|
||||
((ulong)(timestampTicks / Stopwatch.Frequency) * 1_000_000UL) +
|
||||
((ulong)(timestampTicks % Stopwatch.Frequency) * 1_000_000UL / (ulong)Stopwatch.Frequency);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(
|
||||
data[0x50..],
|
||||
timestampMicroseconds);
|
||||
data[0x68] = 1;
|
||||
|
||||
return ctx.Memory.TryWrite(dataAddress, data);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text.RegularExpressions;
|
||||
|
||||
namespace SharpEmu.Libs.PlayGo;
|
||||
|
||||
@@ -14,15 +15,38 @@ public static class PlayGoExports
|
||||
private const int OrbisPlayGoErrorBadHandle = unchecked((int)0x80B20009);
|
||||
private const int OrbisPlayGoErrorBadPointer = unchecked((int)0x80B2000A);
|
||||
private const int OrbisPlayGoErrorBadSize = unchecked((int)0x80B2000B);
|
||||
private const int OrbisPlayGoErrorBadChunkId = unchecked((int)0x80B2000C);
|
||||
private const int OrbisPlayGoErrorNotSupportPlayGo = unchecked((int)0x80B2000E);
|
||||
private const int OrbisPlayGoErrorBadLocus = unchecked((int)0x80B20010);
|
||||
private const ulong PlayGoInitBufAddrOffset = 0;
|
||||
private const ulong PlayGoInitBufSizeOffset = 8;
|
||||
private const uint PlayGoMinimumInitBufferSize = 0x200000;
|
||||
private const uint PlayGoHandle = 1;
|
||||
private const int PlayGoLocusNotDownloaded = 0;
|
||||
private const int PlayGoLocusLocalSlow = 2;
|
||||
private const int PlayGoLocusLocalFast = 3;
|
||||
private const int PlayGoInstallSpeedSuspended = 0;
|
||||
private const int PlayGoInstallSpeedTrickle = 1;
|
||||
private const int PlayGoInstallSpeedFull = 2;
|
||||
private const uint MaxPlayGoQueryEntries = 0x4000;
|
||||
private 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 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(
|
||||
Nid = "ts6GlZOKRrE",
|
||||
@@ -67,8 +91,12 @@ public static class PlayGoExports
|
||||
return OrbisPlayGoErrorAlreadyInitialized;
|
||||
}
|
||||
|
||||
_hasPlayGoData = HasPlayGoChunkData();
|
||||
_metadata = LoadPlayGoMetadata();
|
||||
_installSpeed = PlayGoInstallSpeedTrickle;
|
||||
_languageMask = ulong.MaxValue;
|
||||
_opened = false;
|
||||
_initialized = true;
|
||||
TracePlayGo($"initialize chunks={_metadata.ChunkIds.Length} available={_metadata.Available}");
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -100,10 +128,13 @@ public static class PlayGoExports
|
||||
return OrbisPlayGoErrorNotInitialized;
|
||||
}
|
||||
|
||||
if (!_hasPlayGoData)
|
||||
if (!_metadata.Available)
|
||||
{
|
||||
return OrbisPlayGoErrorNotSupportPlayGo;
|
||||
}
|
||||
|
||||
_opened = true;
|
||||
TracePlayGo($"open handle={PlayGoHandle} chunks={_metadata.ChunkIds.Length}");
|
||||
}
|
||||
|
||||
Span<byte> handleBytes = stackalloc byte[sizeof(uint)];
|
||||
@@ -132,7 +163,8 @@ public static class PlayGoExports
|
||||
}
|
||||
|
||||
_initialized = false;
|
||||
_hasPlayGoData = false;
|
||||
_opened = false;
|
||||
_metadata = PlayGoMetadata.Empty;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
@@ -157,20 +189,611 @@ public static class PlayGoExports
|
||||
{
|
||||
return OrbisPlayGoErrorBadHandle;
|
||||
}
|
||||
|
||||
_opened = false;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool HasPlayGoChunkData()
|
||||
[SysAbiExport(
|
||||
Nid = "73fF1MFU8hA",
|
||||
ExportName = "scePlayGoGetChunkId",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePlayGo")]
|
||||
public static int PlayGoGetChunkId(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
var outChunkIdList = ctx[CpuRegister.Rsi];
|
||||
var numberOfEntries = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||
var outEntries = ctx[CpuRegister.Rcx];
|
||||
|
||||
var validation = ValidateHandle(handle);
|
||||
if (validation != 0)
|
||||
{
|
||||
return validation;
|
||||
}
|
||||
|
||||
if (outEntries == 0)
|
||||
{
|
||||
return OrbisPlayGoErrorBadPointer;
|
||||
}
|
||||
|
||||
if (outChunkIdList != 0 && numberOfEntries == 0)
|
||||
{
|
||||
return OrbisPlayGoErrorBadSize;
|
||||
}
|
||||
|
||||
ushort[] chunkIds;
|
||||
lock (_stateGate)
|
||||
{
|
||||
chunkIds = _metadata.ChunkIds;
|
||||
}
|
||||
|
||||
var availableEntries = chunkIds.Length == 0 ? 1u : (uint)chunkIds.Length;
|
||||
if (outChunkIdList == 0)
|
||||
{
|
||||
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");
|
||||
if (string.IsNullOrWhiteSpace(app0Root))
|
||||
{
|
||||
return PlayGoMetadata.Empty;
|
||||
}
|
||||
|
||||
var playGoDat = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
||||
var scenarioJson = Path.Combine(app0Root, "sce_sys", "playgo-scenario.json");
|
||||
var chunkDefsXml = Path.Combine(app0Root, "playgo-chunkdefs.xml");
|
||||
|
||||
var hasMetadata = File.Exists(playGoDat) || File.Exists(scenarioJson) || File.Exists(chunkDefsXml);
|
||||
if (!hasMetadata)
|
||||
{
|
||||
return PlayGoMetadata.Empty;
|
||||
}
|
||||
|
||||
var chunkIds = LoadChunkIds(chunkDefsXml);
|
||||
return new PlayGoMetadata(true, chunkIds);
|
||||
}
|
||||
|
||||
private static ushort[] LoadChunkIds(string chunkDefsXml)
|
||||
{
|
||||
if (!File.Exists(chunkDefsXml))
|
||||
{
|
||||
return Array.Empty<ushort>();
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var xml = File.ReadAllText(chunkDefsXml);
|
||||
var chunkIds = new HashSet<ushort>();
|
||||
AddChunkIds(xml, DefaultChunkPattern, chunkIds);
|
||||
AddChunkIds(xml, ChunkIdPattern, chunkIds);
|
||||
|
||||
var sorted = chunkIds.ToArray();
|
||||
Array.Sort(sorted);
|
||||
return sorted;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return Array.Empty<ushort>();
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return Array.Empty<ushort>();
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddChunkIds(string xml, Regex pattern, HashSet<ushort> chunkIds)
|
||||
{
|
||||
foreach (Match match in pattern.Matches(xml))
|
||||
{
|
||||
if (ushort.TryParse(match.Groups["id"].Value, out var chunkId))
|
||||
{
|
||||
chunkIds.Add(chunkId);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var hostPath = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
||||
return File.Exists(hostPath);
|
||||
value = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryWriteInt64(CpuContext ctx, ulong address, long value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(long)];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private 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
|
||||
{
|
||||
private const long DateTimeTicksPerMicrosecond = 10;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ZPD1YOKI+Kw",
|
||||
ExportName = "sceRtcGetCurrentClockLocalTime",
|
||||
@@ -42,6 +44,29 @@ public static class RtcExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "18B2NS1y9UU",
|
||||
ExportName = "sceRtcGetCurrentTick",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcGetCurrentTick(CpuContext ctx)
|
||||
{
|
||||
var tickAddress = ctx[CpuRegister.Rdi];
|
||||
if (tickAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
|
||||
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8w-H19ip48I",
|
||||
ExportName = "sceRtcGetTick",
|
||||
@@ -72,14 +97,57 @@ public static class RtcExports
|
||||
rtcDateTime.Minute,
|
||||
rtcDateTime.Second,
|
||||
DateTimeKind.Utc);
|
||||
tickValue = checked((ulong)((baseDateTime.Ticks / 10) + rtcDateTime.Microsecond));
|
||||
tickValue = checked((ulong)((baseDateTime.Ticks / DateTimeTicksPerMicrosecond) + rtcDateTime.Microsecond));
|
||||
}
|
||||
catch (Exception ex) when (ex is ArgumentOutOfRangeException or OverflowException)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ueega6v3GUw",
|
||||
ExportName = "sceRtcSetTick",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceRtc")]
|
||||
public static int RtcSetTick(CpuContext ctx)
|
||||
{
|
||||
var dateTimeAddress = ctx[CpuRegister.Rdi];
|
||||
var tickAddress = ctx[CpuRegister.Rsi];
|
||||
if (dateTimeAddress == 0 || tickAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(tickAddress, out var tickValue))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (tickValue > long.MaxValue / DateTimeTicksPerMicrosecond)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
DateTime dateTime;
|
||||
try
|
||||
{
|
||||
dateTime = new DateTime(checked((long)tickValue * DateTimeTicksPerMicrosecond), DateTimeKind.Utc);
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
||||
if (!TryWriteRtcDateTime(ctx, dateTimeAddress, dateTime))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -108,6 +176,21 @@ public static class RtcExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, DateTime dateTime)
|
||||
{
|
||||
Span<byte> rtcDateTime = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)dateTime.Year));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)dateTime.Month));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)dateTime.Day));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)dateTime.Hour));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)dateTime.Minute));
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)dateTime.Second));
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
||||
rtcDateTime[12..16],
|
||||
checked((uint)((dateTime.Ticks % TimeSpan.TicksPerSecond) / DateTimeTicksPerMicrosecond)));
|
||||
return ctx.Memory.TryWrite(address, rtcDateTime);
|
||||
}
|
||||
|
||||
private readonly record struct RtcDateTime(
|
||||
ushort Year,
|
||||
ushort Month,
|
||||
|
||||
@@ -0,0 +1,213 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.SaveData;
|
||||
|
||||
public static class SaveDataDialogExports
|
||||
{
|
||||
private const int StatusNone = 0;
|
||||
private const int StatusInitialized = 1;
|
||||
private const int StatusRunning = 2;
|
||||
private const int StatusFinished = 3;
|
||||
|
||||
private const int ErrorOk = 0;
|
||||
private const int ErrorNotInitialized = unchecked((int)0x80B80003);
|
||||
private const int ErrorAlreadyInitialized = unchecked((int)0x80B80004);
|
||||
private const int ErrorNotFinished = unchecked((int)0x80B80005);
|
||||
private const int ErrorNotRunning = unchecked((int)0x80B8000B);
|
||||
private const int ErrorArgNull = unchecked((int)0x80B8000D);
|
||||
|
||||
private const int ResultSize = 0x48;
|
||||
private static int _status;
|
||||
private static int _lastMode;
|
||||
private static ulong _lastUserData;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "s9e3+YpRnzw",
|
||||
ExportName = "sceSaveDataDialogInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
|
||||
{
|
||||
return SetReturn(ctx, ErrorAlreadyInitialized);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4tPhsP6FpDI",
|
||||
ExportName = "sceSaveDataDialogOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogOpen(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, ErrorArgNull);
|
||||
}
|
||||
|
||||
if (_status is not (StatusInitialized or StatusFinished))
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotInitialized);
|
||||
}
|
||||
|
||||
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
|
||||
_lastUserData = TryReadUInt64(ctx, paramAddress + 0xC8, out var userData) ? userData : 0;
|
||||
|
||||
// There is no host save dialog yet. Complete immediately with OK so
|
||||
// guest polling sees a finished dialog instead of spinning forever.
|
||||
Interlocked.Exchange(ref _status, StatusFinished);
|
||||
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished");
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ERKzksauAJA",
|
||||
ExportName = "sceSaveDataDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KK3Bdg1RWK0",
|
||||
ExportName = "sceSaveDataDialogUpdateStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "en7gNVnh878",
|
||||
ExportName = "sceSaveDataDialogIsReadyToDisplay",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => SetReturn(ctx, 1);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yEiJ-qqr6Cg",
|
||||
ExportName = "sceSaveDataDialogGetResult",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogGetResult(CpuContext ctx)
|
||||
{
|
||||
var resultAddress = ctx[CpuRegister.Rdi];
|
||||
if (resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) != StatusFinished)
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotFinished);
|
||||
}
|
||||
|
||||
Span<byte> result = stackalloc byte[ResultSize];
|
||||
result.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x00..], _lastMode);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x04..], 0);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(result[0x08..], 0);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(result[0x20..], _lastUserData);
|
||||
|
||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fH46Lag88XY",
|
||||
ExportName = "sceSaveDataDialogClose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogClose(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotRunning);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "YuH2FA7azqQ",
|
||||
ExportName = "sceSaveDataDialogTerminate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogTerminate(CpuContext ctx)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotInitialized);
|
||||
}
|
||||
|
||||
_lastMode = 0;
|
||||
_lastUserData = 0;
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "V-uEeFKARJU",
|
||||
ExportName = "sceSaveDataDialogProgressBarInc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => SetReturn(ctx, ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hay1CfTmLyA",
|
||||
ExportName = "sceSaveDataDialogProgressBarSetValue",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => SetReturn(ctx, ErrorOk);
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
value = 0;
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
value = 0;
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceSaveDataDialog(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] save_data_dialog.{message}");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,444 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.Libs.SaveData;
|
||||
|
||||
public static class SaveDataExports
|
||||
{
|
||||
private const int OrbisSaveDataErrorParameter = unchecked((int)0x809F0000);
|
||||
private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B);
|
||||
private const int SaveDataTitleIdSize = 10;
|
||||
private const int SaveDataDirNameSize = 32;
|
||||
private const int SaveDataParamSize = 0x530;
|
||||
private const int SaveDataSearchInfoSize = 0x30;
|
||||
private const ulong ResultHitNumOffset = 0x00;
|
||||
private const ulong ResultDirNamesOffset = 0x08;
|
||||
private const ulong ResultDirNamesNumOffset = 0x10;
|
||||
private const ulong ResultSetNumOffset = 0x14;
|
||||
private const ulong ResultParamsOffset = 0x18;
|
||||
private const ulong ResultInfosOffset = 0x20;
|
||||
private const uint SortKeyFreeBlocks = 5;
|
||||
private const uint SortOrderDescent = 1;
|
||||
private static readonly object _stateGate = new();
|
||||
private static string? _titleId;
|
||||
|
||||
public static void ConfigureApplicationInfo(string? titleId)
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
_titleId = string.IsNullOrWhiteSpace(titleId) ? null : SanitizePathSegment(titleId.Trim());
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TywrFKCoLGY",
|
||||
ExportName = "sceSaveDataInitialize3",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataInitialize3(CpuContext ctx)
|
||||
{
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ResolveSaveDataRoot());
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "dyIhnXq-0SM",
|
||||
ExportName = "sceSaveDataDirNameSearch",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataDirNameSearch(CpuContext ctx)
|
||||
{
|
||||
var condAddress = ctx[CpuRegister.Rdi];
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
if (condAddress == 0 || resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
|
||||
!TryReadSearchResult(ctx, resultAddress, out var result))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string titleId;
|
||||
if (cond.TitleIdAddress == 0)
|
||||
{
|
||||
titleId = ResolveConfiguredTitleId();
|
||||
}
|
||||
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
|
||||
var entries = Directory.Exists(root)
|
||||
? EnumerateSaveDirectories(root, cond.Pattern)
|
||||
: [];
|
||||
|
||||
entries = SortEntries(entries, cond.SortKey, cond.SortOrder);
|
||||
var setNum = result.DirNamesNum == 0
|
||||
? 0
|
||||
: Math.Min(result.DirNamesNum, entries.Count);
|
||||
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
||||
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (setNum == 0)
|
||||
{
|
||||
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
if (result.DirNamesAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
for (var i = 0; i < setNum; i++)
|
||||
{
|
||||
var entry = entries[i];
|
||||
if (!TryWriteFixedAscii(
|
||||
ctx,
|
||||
result.DirNamesAddress + ((ulong)i * SaveDataDirNameSize),
|
||||
SaveDataDirNameSize,
|
||||
entry.Name) ||
|
||||
(result.ParamsAddress != 0 &&
|
||||
!TryWriteParam(ctx, result.ParamsAddress + ((ulong)i * SaveDataParamSize), entry)) ||
|
||||
(result.InfosAddress != 0 &&
|
||||
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
||||
{
|
||||
cond = default;
|
||||
if (!TryReadInt32(ctx, address, out var userId) ||
|
||||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
||||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
||||
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
|
||||
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string pattern;
|
||||
if (dirNameAddress == 0)
|
||||
{
|
||||
pattern = string.Empty;
|
||||
}
|
||||
else if (!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out pattern))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
cond = new SearchCond(userId, titleIdAddress, pattern, sortKey, sortOrder);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadSearchResult(CpuContext ctx, ulong address, out SearchResult result)
|
||||
{
|
||||
result = default;
|
||||
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
|
||||
!TryReadUInt32(ctx, address + ResultDirNamesNumOffset, out var dirNamesNum) ||
|
||||
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
|
||||
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = new SearchResult(dirNamesAddress, dirNamesNum, paramsAddress, infosAddress);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<SaveEntry> EnumerateSaveDirectories(string root, string pattern)
|
||||
{
|
||||
var entries = new List<SaveEntry>();
|
||||
foreach (var directory in Directory.EnumerateDirectories(root))
|
||||
{
|
||||
var name = Path.GetFileName(directory);
|
||||
if (string.IsNullOrWhiteSpace(name) ||
|
||||
name.StartsWith("sce_", StringComparison.OrdinalIgnoreCase) ||
|
||||
(!string.IsNullOrEmpty(pattern) && !MatchPattern(name, pattern)))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var info = new DirectoryInfo(directory);
|
||||
entries.Add(new SaveEntry(name, directory, info.LastWriteTimeUtc));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
|
||||
private static List<SaveEntry> SortEntries(List<SaveEntry> entries, uint sortKey, uint sortOrder)
|
||||
{
|
||||
IOrderedEnumerable<SaveEntry> sorted = sortKey switch
|
||||
{
|
||||
3 => entries.OrderBy(entry => entry.LastWriteUtc),
|
||||
_ => entries.OrderBy(entry => entry.Name, StringComparer.OrdinalIgnoreCase),
|
||||
};
|
||||
|
||||
var list = sorted.ToList();
|
||||
if (sortOrder == SortOrderDescent)
|
||||
{
|
||||
list.Reverse();
|
||||
}
|
||||
|
||||
return list;
|
||||
}
|
||||
|
||||
private static bool TryWriteParam(CpuContext ctx, ulong address, SaveEntry entry)
|
||||
{
|
||||
var param = new byte[SaveDataParamSize];
|
||||
WriteAscii(param.AsSpan(0x00, 128), "Saved Data");
|
||||
WriteAscii(param.AsSpan(0x100, 1024), entry.Name);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(
|
||||
param.AsSpan(0x508, sizeof(long)),
|
||||
new DateTimeOffset(entry.LastWriteUtc).ToUnixTimeSeconds());
|
||||
return ctx.Memory.TryWrite(address, param);
|
||||
}
|
||||
|
||||
private static bool TryWriteSearchInfo(CpuContext ctx, ulong address, SaveEntry entry)
|
||||
{
|
||||
var size = GetDirectorySize(entry.Path);
|
||||
var usedBlocks = checked((ulong)((size + 32767) / 32768));
|
||||
var blocks = Math.Max(96UL, usedBlocks);
|
||||
Span<byte> info = stackalloc byte[SaveDataSearchInfoSize];
|
||||
info.Clear();
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(info[0x00..], blocks);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(info[0x08..], blocks - usedBlocks);
|
||||
return ctx.Memory.TryWrite(address, info);
|
||||
}
|
||||
|
||||
private static long GetDirectorySize(string root)
|
||||
{
|
||||
long total = 0;
|
||||
foreach (var file in Directory.EnumerateFiles(root, "*", SearchOption.AllDirectories))
|
||||
{
|
||||
total += new FileInfo(file).Length;
|
||||
}
|
||||
|
||||
return total;
|
||||
}
|
||||
|
||||
private static bool MatchPattern(string value, string pattern) =>
|
||||
MatchPattern(value.AsSpan(), pattern.AsSpan());
|
||||
|
||||
private static bool MatchPattern(ReadOnlySpan<char> value, ReadOnlySpan<char> pattern)
|
||||
{
|
||||
if (pattern.IsEmpty)
|
||||
{
|
||||
return value.IsEmpty;
|
||||
}
|
||||
|
||||
if (pattern[0] == '%')
|
||||
{
|
||||
for (var i = 0; i <= value.Length; i++)
|
||||
{
|
||||
if (MatchPattern(value[i..], pattern[1..]))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
if (value.IsEmpty)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pattern[0] == '_' ||
|
||||
char.ToUpperInvariant(pattern[0]) == char.ToUpperInvariant(value[0]))
|
||||
{
|
||||
return MatchPattern(value[1..], pattern[1..]);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static string ResolveTitleSaveRoot(int userId, string titleId) =>
|
||||
Path.Combine(ResolveSaveDataRoot(), userId.ToString(), SanitizePathSegment(titleId));
|
||||
|
||||
private static string ResolveSaveDataRoot()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
||||
var root = string.IsNullOrWhiteSpace(configured)
|
||||
? Path.Combine(Environment.CurrentDirectory, "user", "savedata")
|
||||
: configured;
|
||||
return Path.GetFullPath(root);
|
||||
}
|
||||
|
||||
private static string ResolveConfiguredTitleId()
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (!string.IsNullOrWhiteSpace(_titleId))
|
||||
{
|
||||
return _titleId;
|
||||
}
|
||||
}
|
||||
|
||||
var app0Root = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
|
||||
var app0Name = string.IsNullOrWhiteSpace(app0Root)
|
||||
? null
|
||||
: Path.GetFileName(Path.TrimEndingDirectorySeparator(app0Root));
|
||||
if (!string.IsNullOrWhiteSpace(app0Name))
|
||||
{
|
||||
var candidate = app0Name.Split('-', StringSplitOptions.RemoveEmptyEntries)[0];
|
||||
if (!string.IsNullOrWhiteSpace(candidate))
|
||||
{
|
||||
return SanitizePathSegment(candidate);
|
||||
}
|
||||
}
|
||||
|
||||
return "default";
|
||||
}
|
||||
|
||||
private static string SanitizePathSegment(string value)
|
||||
{
|
||||
var invalid = Path.GetInvalidFileNameChars();
|
||||
var sanitized = new string(value.Select(ch => invalid.Contains(ch) ? '_' : ch).ToArray());
|
||||
return string.IsNullOrWhiteSpace(sanitized) ? "default" : sanitized;
|
||||
}
|
||||
|
||||
private static bool TryReadFixedAscii(CpuContext ctx, ulong address, int length, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
Span<byte> buffer = stackalloc byte[length];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stringLength = buffer.IndexOf((byte)0);
|
||||
if (stringLength < 0)
|
||||
{
|
||||
stringLength = buffer.Length;
|
||||
}
|
||||
|
||||
value = Encoding.ASCII.GetString(buffer[..stringLength]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteFixedAscii(CpuContext ctx, ulong address, int length, string value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[length];
|
||||
buffer.Clear();
|
||||
WriteAscii(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static void WriteAscii(Span<byte> destination, string value)
|
||||
{
|
||||
var count = Math.Min(value.Length, Math.Max(0, destination.Length - 1));
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
var ch = value[i];
|
||||
destination[i] = ch <= 0x7F ? (byte)ch : (byte)'?';
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceSaveData(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] savedata.{message}");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct SearchCond(
|
||||
int UserId,
|
||||
ulong TitleIdAddress,
|
||||
string Pattern,
|
||||
uint SortKey,
|
||||
uint SortOrder);
|
||||
|
||||
private readonly record struct SearchResult(
|
||||
ulong DirNamesAddress,
|
||||
uint DirNamesNum,
|
||||
ulong ParamsAddress,
|
||||
ulong InfosAddress);
|
||||
|
||||
private readonly record struct SaveEntry(string Name, string Path, DateTime LastWriteUtc);
|
||||
}
|
||||
@@ -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}");
|
||||
}
|
||||
}
|
||||
@@ -8,6 +8,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Silk.NET.Vulkan" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
||||
<PackageReference Include="Silk.NET.Windowing" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
// 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.SystemService;
|
||||
|
||||
public static class SystemServiceExports
|
||||
{
|
||||
private const int OrbisSystemServiceErrorParameter = unchecked((int)0x80A10003);
|
||||
private const int SystemServiceStatusSize = 0x0C;
|
||||
private const int DisplaySafeAreaInfoSize = sizeof(float) + 128;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fZo48un7LK4",
|
||||
ExportName = "sceSystemServiceParamGetInt",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceParamGetInt(CpuContext ctx)
|
||||
{
|
||||
var parameterId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var valueAddress = ctx[CpuRegister.Rsi];
|
||||
if (valueAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
var value = parameterId switch
|
||||
{
|
||||
1 or 2 or 3 or 1000 => 1,
|
||||
4 => 180,
|
||||
_ => 0,
|
||||
};
|
||||
|
||||
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||
return ctx.Memory.TryWrite(valueAddress, valueBytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rPo6tV8D9bM",
|
||||
ExportName = "sceSystemServiceGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSystemService")]
|
||||
public static int SystemServiceGetStatus(CpuContext ctx)
|
||||
{
|
||||
var statusAddress = ctx[CpuRegister.Rdi];
|
||||
if (statusAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> status = stackalloc byte[SystemServiceStatusSize];
|
||||
status.Clear();
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status, 0);
|
||||
status[0x06] = 1;
|
||||
|
||||
return ctx.Memory.TryWrite(statusAddress, status)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,169 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.UserService;
|
||||
|
||||
public static class UserServiceExports
|
||||
{
|
||||
private const int OrbisUserServiceErrorInvalidArgument = unchecked((int)0x80960005);
|
||||
private const int OrbisUserServiceErrorNoEvent = unchecked((int)0x80960007);
|
||||
private const int OrbisUserServiceErrorInvalidParameter = unchecked((int)0x80960009);
|
||||
private const int OrbisUserServiceErrorBufferTooShort = unchecked((int)0x8096000A);
|
||||
private const int PrimaryUserId = 1;
|
||||
private const int InvalidUserId = -1;
|
||||
private const string PrimaryUserName = "SharpEmu";
|
||||
private static int _loginEventDelivered;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "j3YMu1MVNNo",
|
||||
ExportName = "sceUserServiceInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceInitialize(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "CdWp0oHWGr0",
|
||||
ExportName = "sceUserServiceGetInitialUser",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceGetInitialUser(CpuContext ctx)
|
||||
{
|
||||
var userIdAddress = ctx[CpuRegister.Rdi];
|
||||
if (userIdAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fPhymKNvK-A",
|
||||
ExportName = "sceUserServiceGetLoginUserIdList",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceGetLoginUserIdList(CpuContext ctx)
|
||||
{
|
||||
var userIdListAddress = ctx[CpuRegister.Rdi];
|
||||
if (userIdListAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x00..], PrimaryUserId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x04..], InvalidUserId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
|
||||
return ctx.Memory.TryWrite(userIdListAddress, userIds)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yH17Q6NWtVg",
|
||||
ExportName = "sceUserServiceGetEvent",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceGetEvent(CpuContext ctx)
|
||||
{
|
||||
var eventAddress = ctx[CpuRegister.Rdi];
|
||||
if (eventAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorNoEvent);
|
||||
}
|
||||
|
||||
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
|
||||
return ctx.Memory.TryWrite(eventAddress, payload)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1xxcMiGu2fo",
|
||||
ExportName = "sceUserServiceGetUserName",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceGetUserName(CpuContext ctx)
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
var capacity = ctx[CpuRegister.Rdx];
|
||||
if (userId != PrimaryUserId)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||
}
|
||||
|
||||
if (nameAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
|
||||
if (capacity <= (ulong)nameBytes.Length)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort);
|
||||
}
|
||||
|
||||
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
||||
nameBytes.CopyTo(output);
|
||||
return ctx.Memory.TryWrite(nameAddress, output)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "D-CzAxQL0XI",
|
||||
ExportName = "sceUserServiceGetPlatformPrivacySetting",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceUserService")]
|
||||
public static int UserServiceGetPlatformPrivacySetting(CpuContext ctx)
|
||||
{
|
||||
var parameterId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var valueAddress = ctx[CpuRegister.Rsi];
|
||||
if (parameterId != 1000)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||
}
|
||||
|
||||
if (valueAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
return TryWriteInt32(ctx, valueAddress, 0)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
@@ -15,28 +16,116 @@ public static class VideoOutExports
|
||||
private const int OrbisVideoOutErrorResourceBusy = unchecked((int)0x80290009);
|
||||
private const int OrbisVideoOutErrorInvalidIndex = unchecked((int)0x8029000A);
|
||||
private const int OrbisVideoOutErrorInvalidHandle = unchecked((int)0x8029000B);
|
||||
private const int OrbisVideoOutErrorInvalidEventQueue = unchecked((int)0x8029000C);
|
||||
private const int OrbisVideoOutErrorInvalidEvent = unchecked((int)0x8029000D);
|
||||
private const int OrbisVideoOutErrorInvalidOption = unchecked((int)0x8029001A);
|
||||
private const int SceVideoOutBusTypeMain = 0;
|
||||
private const int SceVideoOutBufferAttributeOptionNone = 0;
|
||||
private const int SceVideoOutTilingModeLinear = 1;
|
||||
private const int MaxOpenPorts = 4;
|
||||
private const int MaxDisplayBuffers = 16;
|
||||
private const int VideoOutBufferAttributeSize = 0x24;
|
||||
private const int MaxDisplayBufferGroups = 4;
|
||||
private const int MaxFrameDumps = 8;
|
||||
private const int VideoOutBufferAttributeSize = 0x28;
|
||||
private const int VideoOutBufferAttribute2Size = 0x50;
|
||||
private const int VideoOutBuffersEntrySize = 0x20;
|
||||
private const int VideoOutOutputStatusSize = 0x30;
|
||||
private const ulong SceVideoOutPixelFormatA8R8G8B8Srgb = 0x80000000;
|
||||
private const ulong SceVideoOutPixelFormatA8B8G8R8Srgb = 0x80002200;
|
||||
private const ulong SceVideoOutPixelFormatB8G8R8A8Unorm = 0x8100000000000000;
|
||||
private const ulong SceVideoOutPixelFormatR8G8B8A8Unorm = 0x8100000022000000;
|
||||
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
|
||||
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
|
||||
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
|
||||
private const ulong SceVideoOutInternalEventFlip = 0x6;
|
||||
private const short OrbisKernelEventFilterVideoOut = -13;
|
||||
|
||||
private static readonly object _stateGate = new();
|
||||
private static readonly object _frameDumpGate = new();
|
||||
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
|
||||
private static readonly Dictionary<(int Handle, int BufferIndex, ulong Address), ulong> _lastFrameFingerprints = new();
|
||||
private static int _nextHandle = 1;
|
||||
private static int _frameDumpCount;
|
||||
private static long _nextFrameDumpIndex;
|
||||
private 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
|
||||
{
|
||||
public required int Handle { get; init; }
|
||||
public int FlipRate { get; set; }
|
||||
public ulong VblankCount { get; set; }
|
||||
public int NextSetId { get; set; } = 1;
|
||||
public HashSet<int> RegisteredSetIds { get; } = new();
|
||||
public ulong FlipCount { get; set; }
|
||||
public int CurrentBuffer { get; set; } = -1;
|
||||
public uint OutputWidth { get; set; } = 1920;
|
||||
public uint OutputHeight { get; set; } = 1080;
|
||||
public uint RefreshRate { get; set; } = 60;
|
||||
public float Gamma { get; set; } = 1.0f;
|
||||
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
|
||||
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
|
||||
public List<FlipEventRegistration> FlipEvents { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class VideoOutBufferGroup
|
||||
{
|
||||
public required int Index { get; init; }
|
||||
public required BufferAttribute Attribute { get; init; }
|
||||
}
|
||||
|
||||
private sealed class VideoOutBufferSlot
|
||||
{
|
||||
public int GroupIndex { get; set; } = -1;
|
||||
public ulong AddressLeft { get; set; }
|
||||
public ulong AddressRight { get; set; }
|
||||
}
|
||||
|
||||
private readonly record struct FlipEventRegistration(ulong Equeue, ulong UserData);
|
||||
|
||||
private readonly record struct BufferAttribute(
|
||||
ulong PixelFormat,
|
||||
uint TilingMode,
|
||||
uint AspectRatio,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint PitchInPixel,
|
||||
ulong Option);
|
||||
|
||||
internal readonly record struct DisplayBufferInfo(
|
||||
ulong Address,
|
||||
ulong PixelFormat,
|
||||
uint TilingMode,
|
||||
uint Width,
|
||||
uint Height,
|
||||
uint PitchInPixel);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Up36PTk687E",
|
||||
ExportName = "sceVideoOutOpen",
|
||||
@@ -114,6 +203,93 @@ public static class VideoOutExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "utPrVdxio-8",
|
||||
ExportName = "sceVideoOutGetOutputStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutGetOutputStatus(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var statusAddress = ctx[CpuRegister.Rsi];
|
||||
if (statusAddress == 0)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidAddress;
|
||||
}
|
||||
|
||||
if (!TryGetPort(handle, out var port))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
Span<byte> status = stackalloc byte[VideoOutOutputStatusSize];
|
||||
status.Clear();
|
||||
var resolutionClass = port.OutputWidth >= 3840 || port.OutputHeight >= 2160 ? 2 : 1;
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status[0x00..0x04], resolutionClass);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status[0x04..0x08], 1);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(status[0x08..0x10], port.RefreshRate);
|
||||
return ctx.Memory.TryWrite(statusAddress, status)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "DYhhWbJSeRg",
|
||||
ExportName = "sceVideoOutColorSettingsSetGamma_",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutColorSettingsSetGamma(CpuContext ctx)
|
||||
{
|
||||
var settingsAddress = ctx[CpuRegister.Rdi];
|
||||
if (settingsAddress == 0)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidAddress;
|
||||
}
|
||||
|
||||
ctx.GetXmmRegister(0, out var xmm0Low, out _);
|
||||
var gamma = BitConverter.Int32BitsToSingle(unchecked((int)xmm0Low));
|
||||
if (!float.IsFinite(gamma) || gamma is < 0.1f or > 2.0f)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
Span<byte> gammaBytes = stackalloc byte[sizeof(float)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(gammaBytes, BitConverter.SingleToInt32Bits(gamma));
|
||||
return ctx.Memory.TryWrite(settingsAddress, gammaBytes)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "pv9CI5VC+R0",
|
||||
ExportName = "sceVideoOutAdjustColor_",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutAdjustColor(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var settingsAddress = ctx[CpuRegister.Rsi];
|
||||
if (settingsAddress == 0)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidAddress;
|
||||
}
|
||||
|
||||
if (!TryGetPort(handle, out var port))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
Span<byte> gammaBytes = stackalloc byte[sizeof(float)];
|
||||
if (!ctx.Memory.TryRead(settingsAddress, gammaBytes))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
port.Gamma = BitConverter.Int32BitsToSingle(
|
||||
BinaryPrimitives.ReadInt32LittleEndian(gammaBytes));
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "j6RaAUlaLv0",
|
||||
ExportName = "sceVideoOutWaitVblank",
|
||||
@@ -136,6 +312,191 @@ public static class VideoOutExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "HXzjK9yI30k",
|
||||
ExportName = "sceVideoOutAddFlipEvent",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutAddFlipEvent(CpuContext ctx)
|
||||
{
|
||||
var equeue = ctx[CpuRegister.Rdi];
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var userData = ctx[CpuRegister.Rdx];
|
||||
if (!TryGetPort(handle, out var port))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
if (!KernelEventQueueCompatExports.IsValidEqueue(equeue))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidEventQueue;
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
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}");
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "U46NwOiJpys",
|
||||
ExportName = "sceVideoOutSubmitFlip",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutSubmitFlip(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var bufferIndex = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var flipMode = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var flipArg = unchecked((long)ctx[CpuRegister.Rcx]);
|
||||
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "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(
|
||||
Nid = "U2JJtSqNKZI",
|
||||
ExportName = "sceVideoOutGetEventId",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutGetEventId(CpuContext ctx)
|
||||
{
|
||||
var eventAddress = ctx[CpuRegister.Rdi];
|
||||
if (eventAddress == 0)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidAddress;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(eventAddress, out var ident) ||
|
||||
!TryReadInt16(ctx, eventAddress + 0x08, out var filter))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidEvent;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rWUTcKdkUzQ",
|
||||
ExportName = "sceVideoOutGetEventData",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceVideoOut")]
|
||||
public static int VideoOutGetEventData(CpuContext ctx)
|
||||
{
|
||||
var eventAddress = ctx[CpuRegister.Rdi];
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
if (eventAddress == 0 || dataAddress == 0)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidAddress;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(eventAddress, out var ident) ||
|
||||
!TryReadInt16(ctx, eventAddress + 0x08, out var filter) ||
|
||||
!ctx.TryReadUInt64(eventAddress + 0x10, out var data))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidEvent;
|
||||
}
|
||||
|
||||
var decodedData = unchecked((ulong)(unchecked((long)data) >> 16));
|
||||
return ctx.TryWriteUInt64(dataAddress, decodedData)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
public static int SubmitFlipFromAgc(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg) =>
|
||||
SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
|
||||
|
||||
internal static void SubmitHostRgbaFrame(ReadOnlySpan<byte> rgbaFrame, uint width, uint height)
|
||||
{
|
||||
if (rgbaFrame.Length != checked((int)(width * height * 4)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var bgraFrame = new byte[rgbaFrame.Length];
|
||||
for (var offset = 0; offset < rgbaFrame.Length; offset += 4)
|
||||
{
|
||||
bgraFrame[offset + 0] = rgbaFrame[offset + 2];
|
||||
bgraFrame[offset + 1] = rgbaFrame[offset + 1];
|
||||
bgraFrame[offset + 2] = rgbaFrame[offset + 0];
|
||||
bgraFrame[offset + 3] = rgbaFrame[offset + 3];
|
||||
}
|
||||
|
||||
VulkanVideoPresenter.Submit(bgraFrame, width, height);
|
||||
}
|
||||
|
||||
internal static bool TryGetDisplayBufferInfo(int handle, int bufferIndex, out DisplayBufferInfo info)
|
||||
{
|
||||
info = default;
|
||||
if (bufferIndex < 0 || bufferIndex >= MaxDisplayBuffers)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (!_ports.TryGetValue(handle, out var port))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var slot = port.BufferSlots[bufferIndex];
|
||||
if (slot.AddressLeft == 0 ||
|
||||
slot.GroupIndex < 0 ||
|
||||
slot.GroupIndex >= port.Groups.Length ||
|
||||
port.Groups[slot.GroupIndex] is not { } group)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var attribute = group.Attribute;
|
||||
info = new DisplayBufferInfo(
|
||||
slot.AddressLeft,
|
||||
attribute.PixelFormat,
|
||||
attribute.TilingMode,
|
||||
attribute.Width,
|
||||
attribute.Height,
|
||||
attribute.PitchInPixel);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MTxxrOCeSig",
|
||||
ExportName = "sceVideoOutSetWindowModeMargins",
|
||||
@@ -173,9 +534,23 @@ public static class VideoOutExports
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
return port.RegisteredSetIds.Remove(attributeIndex)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: OrbisVideoOutErrorInvalidValue;
|
||||
if (attributeIndex >= port.Groups.Length || port.Groups[attributeIndex] is null)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
port.Groups[attributeIndex] = null;
|
||||
foreach (var slot in port.BufferSlots)
|
||||
{
|
||||
if (slot.GroupIndex == attributeIndex)
|
||||
{
|
||||
slot.GroupIndex = -1;
|
||||
slot.AddressLeft = 0;
|
||||
slot.AddressRight = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -292,16 +667,21 @@ public static class VideoOutExports
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
if (!TryReadBufferAttribute(ctx, attributeAddress, false, out var attribute))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
Span<ulong> addresses = stackalloc ulong[Math.Min(bufferNum, MaxDisplayBuffers)];
|
||||
for (var i = 0; i < bufferNum; i++)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out _))
|
||||
if (!ctx.TryReadUInt64(addressesAddress + ((ulong)i * 8), out addresses[i]))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
|
||||
var setId = AllocateBufferSet(port);
|
||||
return setId;
|
||||
return RegisterBufferRange(port, startIndex, addresses[..bufferNum], attribute);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -348,34 +728,465 @@ public static class VideoOutExports
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
if (!TryReadBufferAttribute(ctx, attributeAddress, true, out var attribute))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
Span<ulong> addresses = stackalloc ulong[Math.Min(bufferNum, MaxDisplayBuffers)];
|
||||
for (var i = 0; i < bufferNum; i++)
|
||||
{
|
||||
var entryAddress = buffersAddress + ((ulong)i * VideoOutBuffersEntrySize);
|
||||
if (!ctx.TryReadUInt64(entryAddress + 0x00, out _) ||
|
||||
if (!ctx.TryReadUInt64(entryAddress + 0x00, out addresses[i]) ||
|
||||
!ctx.TryReadUInt64(entryAddress + 0x08, out _))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
port.RegisteredSetIds.Add(setIndex);
|
||||
}
|
||||
|
||||
return setIndex;
|
||||
var groupIndex = RegisterBufferRange(port, bufferIndexStart, addresses[..bufferNum], attribute, setIndex);
|
||||
return groupIndex < 0 ? groupIndex : setIndex;
|
||||
}
|
||||
|
||||
private static int AllocateBufferSet(VideoOutPortState port)
|
||||
private static int SubmitFlip(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg)
|
||||
{
|
||||
if (!TryGetPort(handle, out var port))
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidHandle;
|
||||
}
|
||||
|
||||
if (bufferIndex < -1 || bufferIndex >= MaxDisplayBuffers)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidIndex;
|
||||
}
|
||||
|
||||
ulong eventHint;
|
||||
List<FlipEventRegistration> flipEvents;
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (bufferIndex != -1 && port.BufferSlots[bufferIndex].GroupIndex < 0)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidIndex;
|
||||
}
|
||||
|
||||
port.CurrentBuffer = bufferIndex;
|
||||
port.FlipCount++;
|
||||
eventHint = SceVideoOutInternalEventFlip |
|
||||
((unchecked((ulong)flipArg) & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
|
||||
}
|
||||
|
||||
if (string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
_ = TryDumpFrame(ctx, port, bufferIndex, flipMode, flipArg);
|
||||
}
|
||||
|
||||
foreach (var flipEvent in flipEvents)
|
||||
{
|
||||
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||
flipEvent.Equeue,
|
||||
SceVideoOutInternalEventFlip,
|
||||
OrbisKernelEventFilterVideoOut,
|
||||
eventHint,
|
||||
flipEvent.UserData);
|
||||
}
|
||||
|
||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
|
||||
{
|
||||
lock (_stateGate)
|
||||
{
|
||||
var setId = port.NextSetId++;
|
||||
port.RegisteredSetIds.Add(setId);
|
||||
return setId;
|
||||
var groupIndex = requestedGroupIndex >= 0 ? requestedGroupIndex : FindFreeGroupIndex(port);
|
||||
if (groupIndex < 0 || groupIndex >= MaxDisplayBufferGroups)
|
||||
{
|
||||
return OrbisVideoOutErrorInvalidValue;
|
||||
}
|
||||
|
||||
if (port.Groups[groupIndex] is not null)
|
||||
{
|
||||
return OrbisVideoOutErrorResourceBusy;
|
||||
}
|
||||
|
||||
for (var i = 0; i < addresses.Length; i++)
|
||||
{
|
||||
if (port.BufferSlots[startIndex + i].GroupIndex >= 0)
|
||||
{
|
||||
return OrbisVideoOutErrorResourceBusy;
|
||||
}
|
||||
}
|
||||
|
||||
port.Groups[groupIndex] = new VideoOutBufferGroup
|
||||
{
|
||||
Index = groupIndex,
|
||||
Attribute = attribute,
|
||||
};
|
||||
port.OutputWidth = attribute.Width;
|
||||
port.OutputHeight = attribute.Height;
|
||||
|
||||
for (var i = 0; i < addresses.Length; i++)
|
||||
{
|
||||
var slot = port.BufferSlots[startIndex + i];
|
||||
slot.GroupIndex = groupIndex;
|
||||
slot.AddressLeft = addresses[i];
|
||||
slot.AddressRight = 0;
|
||||
}
|
||||
|
||||
TraceVideoOut(
|
||||
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
|
||||
return groupIndex;
|
||||
}
|
||||
}
|
||||
|
||||
private static int FindFreeGroupIndex(VideoOutPortState port)
|
||||
{
|
||||
for (var i = 0; i < port.Groups.Length; i++)
|
||||
{
|
||||
if (port.Groups[i] is null)
|
||||
{
|
||||
return i;
|
||||
}
|
||||
}
|
||||
|
||||
return -1;
|
||||
}
|
||||
|
||||
private static bool TryReadBufferAttribute(CpuContext ctx, ulong attributeAddress, bool attribute2, out BufferAttribute attribute)
|
||||
{
|
||||
attribute = default;
|
||||
if (!TryReadUInt32(ctx, attributeAddress + 0x04, out var tilingMode) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x0C, out var width) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x10, out var height))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (attribute2)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(attributeAddress + 0x18, out var option) ||
|
||||
!ctx.TryReadUInt64(attributeAddress + 0x20, out var pixelFormat))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
attribute = new BufferAttribute(NormalizePixelFormat(pixelFormat), tilingMode, 0, width, height, width, option);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryReadUInt32(ctx, attributeAddress + 0x00, out var pixelFormat32) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x08, out var aspectRatio) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x14, out var pitchInPixel) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x18, out var option32))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
attribute = new BufferAttribute(NormalizePixelFormat(pixelFormat32), tilingMode, aspectRatio, width, height, pitchInPixel, option32);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryDumpFrame(CpuContext ctx, VideoOutPortState port, int bufferIndex, int flipMode, long flipArg)
|
||||
{
|
||||
if (bufferIndex < 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
VideoOutBufferSlot slot;
|
||||
VideoOutBufferGroup? group;
|
||||
lock (_stateGate)
|
||||
{
|
||||
slot = port.BufferSlots[bufferIndex];
|
||||
group = slot.GroupIndex >= 0 && slot.GroupIndex < port.Groups.Length
|
||||
? port.Groups[slot.GroupIndex]
|
||||
: null;
|
||||
}
|
||||
|
||||
if (group is null || slot.AddressLeft == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var attribute = group.Attribute;
|
||||
if (attribute.Width == 0 || attribute.Height == 0 || attribute.Width > 8192 || attribute.Height > 8192)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytesPerPixel = GetBytesPerPixel(attribute.PixelFormat);
|
||||
if (bytesPerPixel == 0)
|
||||
{
|
||||
return DumpRawFrame(ctx, port.Handle, slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "unsupported-format");
|
||||
}
|
||||
|
||||
var pitch = attribute.PitchInPixel == 0 ? attribute.Width : attribute.PitchInPixel;
|
||||
var rowBytes = checked((int)(pitch * bytesPerPixel));
|
||||
var visibleRowBytes = checked((int)(attribute.Width * bytesPerPixel));
|
||||
var frameBytes = checked((ulong)rowBytes * attribute.Height);
|
||||
if (frameBytes > 256UL * 1024UL * 1024UL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_frameDumpGate)
|
||||
{
|
||||
if (_frameDumpCount >= MaxFrameDumps)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
const ulong fnvOffsetBasis = 14695981039346656037UL;
|
||||
const ulong fnvPrime = 1099511628211UL;
|
||||
var fingerprint = fnvOffsetBasis;
|
||||
var row = new byte[rowBytes];
|
||||
for (uint y = 0; y < attribute.Height; y++)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(slot.AddressLeft + ((ulong)y * (ulong)rowBytes), row))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
foreach (var value in row.AsSpan(0, visibleRowBytes))
|
||||
{
|
||||
fingerprint = (fingerprint ^ value) * fnvPrime;
|
||||
}
|
||||
}
|
||||
|
||||
var fingerprintKey = (port.Handle, bufferIndex, slot.AddressLeft);
|
||||
lock (_frameDumpGate)
|
||||
{
|
||||
if (_lastFrameFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) &&
|
||||
previousFingerprint == fingerprint)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (_frameDumpCount >= MaxFrameDumps)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_lastFrameFingerprints[fingerprintKey] = fingerprint;
|
||||
_frameDumpCount++;
|
||||
}
|
||||
|
||||
var rgb = new byte[checked((int)(attribute.Width * attribute.Height * 3))];
|
||||
var rgbOffset = 0;
|
||||
for (uint y = 0; y < attribute.Height; y++)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(slot.AddressLeft + ((ulong)y * (ulong)rowBytes), row))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ConvertRowToRgb(row.AsSpan(0, visibleRowBytes), rgb.AsSpan(rgbOffset, (int)attribute.Width * 3), attribute.PixelFormat);
|
||||
rgbOffset += (int)attribute.Width * 3;
|
||||
}
|
||||
|
||||
var frameIndex = Interlocked.Increment(ref _nextFrameDumpIndex);
|
||||
var basePath = GetFrameDumpBasePath(frameIndex, port.Handle, bufferIndex);
|
||||
WriteBmp(basePath + ".bmp", attribute.Width, attribute.Height, rgb);
|
||||
WriteFrameMetadata(basePath + ".txt", slot.AddressLeft, attribute, bufferIndex, flipMode, flipArg, "bmp-linear-read", fingerprint);
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.bmp addr=0x{slot.AddressLeft:X16} {attribute.Width}x{attribute.Height} fmt=0x{attribute.PixelFormat:X} fingerprint=0x{fingerprint:X16}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool DumpRawFrame(CpuContext ctx, int handle, ulong address, BufferAttribute attribute, int bufferIndex, int flipMode, long flipArg, string reason)
|
||||
{
|
||||
var bytesPerPixel = Math.Max(GetBytesPerPixel(attribute.PixelFormat), 4u);
|
||||
var pitch = attribute.PitchInPixel == 0 ? attribute.Width : attribute.PitchInPixel;
|
||||
var byteCount = checked((ulong)pitch * attribute.Height * bytesPerPixel);
|
||||
if (byteCount == 0 || byteCount > 256UL * 1024UL * 1024UL)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[(int)byteCount];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var fingerprint = ComputeFingerprint(bytes);
|
||||
var fingerprintKey = (handle, bufferIndex, address);
|
||||
lock (_frameDumpGate)
|
||||
{
|
||||
if ((_lastFrameFingerprints.TryGetValue(fingerprintKey, out var previousFingerprint) &&
|
||||
previousFingerprint == fingerprint) ||
|
||||
_frameDumpCount >= MaxFrameDumps)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_lastFrameFingerprints[fingerprintKey] = fingerprint;
|
||||
_frameDumpCount++;
|
||||
}
|
||||
|
||||
var frameIndex = Interlocked.Increment(ref _nextFrameDumpIndex);
|
||||
var basePath = GetFrameDumpBasePath(frameIndex, handle, bufferIndex);
|
||||
File.WriteAllBytes(basePath + ".raw", bytes);
|
||||
WriteFrameMetadata(basePath + ".txt", address, attribute, bufferIndex, flipMode, flipArg, reason, fingerprint);
|
||||
TraceVideoOut($"videoout.dump_frame path={basePath}.raw addr=0x{address:X16} bytes={byteCount} reason={reason} fingerprint=0x{fingerprint:X16}");
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong ComputeFingerprint(ReadOnlySpan<byte> bytes)
|
||||
{
|
||||
const ulong fnvOffsetBasis = 14695981039346656037UL;
|
||||
const ulong fnvPrime = 1099511628211UL;
|
||||
var fingerprint = fnvOffsetBasis;
|
||||
foreach (var value in bytes)
|
||||
{
|
||||
fingerprint = (fingerprint ^ value) * fnvPrime;
|
||||
}
|
||||
|
||||
return fingerprint;
|
||||
}
|
||||
|
||||
private static uint GetBytesPerPixel(ulong pixelFormat) =>
|
||||
pixelFormat is SceVideoOutPixelFormatA8R8G8B8Srgb or
|
||||
SceVideoOutPixelFormatA8B8G8R8Srgb or
|
||||
SceVideoOutPixelFormatB8G8R8A8Unorm or
|
||||
SceVideoOutPixelFormatR8G8B8A8Unorm or
|
||||
SceVideoOutPixelFormatA2R10G10B10 or
|
||||
SceVideoOutPixelFormatA2R10G10B10Srgb or
|
||||
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq
|
||||
? 4u
|
||||
: 0u;
|
||||
|
||||
private static ulong NormalizePixelFormat(ulong pixelFormat)
|
||||
{
|
||||
if (GetBytesPerPixel(pixelFormat) != 0)
|
||||
{
|
||||
return pixelFormat;
|
||||
}
|
||||
|
||||
var low = (uint)(pixelFormat & 0xFFFF_FFFFUL);
|
||||
if (GetBytesPerPixel(low) != 0)
|
||||
{
|
||||
return low;
|
||||
}
|
||||
|
||||
var high = (uint)(pixelFormat >> 32);
|
||||
if (GetBytesPerPixel(high) != 0)
|
||||
{
|
||||
return high;
|
||||
}
|
||||
|
||||
var packed = high | (low >> 16);
|
||||
return GetBytesPerPixel(packed) != 0 ? packed : pixelFormat;
|
||||
}
|
||||
|
||||
private static void ConvertRowToRgb(ReadOnlySpan<byte> source, Span<byte> destination, ulong pixelFormat)
|
||||
{
|
||||
var dst = 0;
|
||||
for (var src = 0; src + 3 < source.Length; src += 4)
|
||||
{
|
||||
if (pixelFormat is SceVideoOutPixelFormatA8B8G8R8Srgb or SceVideoOutPixelFormatR8G8B8A8Unorm)
|
||||
{
|
||||
destination[dst++] = source[src + 0];
|
||||
destination[dst++] = source[src + 1];
|
||||
destination[dst++] = source[src + 2];
|
||||
}
|
||||
else if (pixelFormat is SceVideoOutPixelFormatA2R10G10B10 or SceVideoOutPixelFormatA2R10G10B10Srgb or SceVideoOutPixelFormatA2R10G10B10Bt2020Pq)
|
||||
{
|
||||
var value = BinaryPrimitives.ReadUInt32LittleEndian(source[src..(src + 4)]);
|
||||
destination[dst++] = (byte)(((value >> 20) & 0x3FF) >> 2);
|
||||
destination[dst++] = (byte)(((value >> 10) & 0x3FF) >> 2);
|
||||
destination[dst++] = (byte)((value & 0x3FF) >> 2);
|
||||
}
|
||||
else
|
||||
{
|
||||
destination[dst++] = source[src + 2];
|
||||
destination[dst++] = source[src + 1];
|
||||
destination[dst++] = source[src + 0];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string GetFrameDumpBasePath(long frameIndex, int handle, int bufferIndex)
|
||||
{
|
||||
var directory = GetLogsDirectory();
|
||||
Directory.CreateDirectory(directory);
|
||||
return Path.Combine(directory, $"videoout_frame_{frameIndex:D4}_h{handle}_b{bufferIndex}");
|
||||
}
|
||||
|
||||
private static string GetLogsDirectory()
|
||||
{
|
||||
var current = new DirectoryInfo(AppContext.BaseDirectory);
|
||||
while (current is not null)
|
||||
{
|
||||
if (File.Exists(Path.Combine(current.FullName, "SharpEmu.slnx")))
|
||||
{
|
||||
return Path.Combine(current.FullName, "logs");
|
||||
}
|
||||
|
||||
current = current.Parent;
|
||||
}
|
||||
|
||||
return Path.Combine(Directory.GetCurrentDirectory(), "logs");
|
||||
}
|
||||
|
||||
private static void WriteBmp(string path, uint width, uint height, byte[] rgb)
|
||||
{
|
||||
var rowStride = checked((int)(((width * 3u) + 3u) & ~3u));
|
||||
var pixelBytes = checked(rowStride * (int)height);
|
||||
var fileSize = 54 + pixelBytes;
|
||||
using var stream = File.Create(path);
|
||||
Span<byte> header = stackalloc byte[54];
|
||||
header[0] = (byte)'B';
|
||||
header[1] = (byte)'M';
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header[0x02..], (uint)fileSize);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header[0x0A..], 54);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header[0x0E..], 40);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(header[0x12..], (int)width);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(header[0x16..], -(int)height);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(header[0x1A..], 1);
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(header[0x1C..], 24);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(header[0x22..], (uint)pixelBytes);
|
||||
stream.Write(header);
|
||||
|
||||
var row = new byte[rowStride];
|
||||
var sourceStride = (int)width * 3;
|
||||
var heightInt = (int)height;
|
||||
var widthInt = (int)width;
|
||||
for (var y = 0; y < heightInt; y++)
|
||||
{
|
||||
row.AsSpan().Clear();
|
||||
var src = rgb.AsSpan(y * sourceStride, sourceStride);
|
||||
for (var x = 0; x < widthInt; x++)
|
||||
{
|
||||
row[(x * 3) + 0] = src[(x * 3) + 2];
|
||||
row[(x * 3) + 1] = src[(x * 3) + 1];
|
||||
row[(x * 3) + 2] = src[(x * 3) + 0];
|
||||
}
|
||||
|
||||
stream.Write(row);
|
||||
}
|
||||
}
|
||||
|
||||
private static void WriteFrameMetadata(
|
||||
string path,
|
||||
ulong address,
|
||||
BufferAttribute attribute,
|
||||
int bufferIndex,
|
||||
int flipMode,
|
||||
long flipArg,
|
||||
string kind,
|
||||
ulong fingerprint)
|
||||
{
|
||||
File.WriteAllText(
|
||||
path,
|
||||
$"kind={kind}\naddress=0x{address:X16}\nbuffer_index={bufferIndex}\nflip_mode={flipMode}\nflip_arg={flipArg}\nfingerprint=0x{fingerprint:X16}\npixel_format=0x{attribute.PixelFormat:X}\ntiling_mode={attribute.TilingMode}\nwidth={attribute.Width}\nheight={attribute.Height}\npitch_in_pixel={attribute.PitchInPixel}\noption=0x{attribute.Option:X}\n");
|
||||
}
|
||||
|
||||
private static bool IsValidBufferRange(int startIndex, int bufferNum)
|
||||
{
|
||||
return startIndex >= 0 &&
|
||||
@@ -393,6 +1204,17 @@ public static class VideoOutExports
|
||||
}
|
||||
}
|
||||
|
||||
private static VideoOutBufferSlot[] CreateBufferSlots()
|
||||
{
|
||||
var slots = new VideoOutBufferSlot[MaxDisplayBuffers];
|
||||
for (var i = 0; i < slots.Length; i++)
|
||||
{
|
||||
slots[i] = new VideoOutBufferSlot();
|
||||
}
|
||||
|
||||
return slots;
|
||||
}
|
||||
|
||||
private static bool TryReadStackUInt32(CpuContext ctx, int stackIndex, out uint value)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rsp] + 0x08 + ((ulong)stackIndex * 0x08);
|
||||
@@ -406,4 +1228,40 @@ public static class VideoOutExports
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadInt16(CpuContext ctx, ulong address, out short value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(short)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt16LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceVideoOut(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,91 @@
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Silk.NET.Vulkan": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing": {
|
||||
"type": "Direct",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
|
||||
},
|
||||
"Microsoft.Extensions.DependencyModel": {
|
||||
"type": "Transitive",
|
||||
"resolved": "9.0.9",
|
||||
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
|
||||
},
|
||||
"Silk.NET.Core": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
|
||||
"dependencies": {
|
||||
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
|
||||
"Microsoft.Extensions.DependencyModel": "9.0.9"
|
||||
}
|
||||
},
|
||||
"Silk.NET.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Ultz.Native.GLFW": "3.4.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Maths": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
|
||||
},
|
||||
"Silk.NET.Windowing.Common": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Maths": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Windowing.Glfw": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
|
||||
"dependencies": {
|
||||
"Silk.NET.GLFW": "2.23.0",
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user