mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 48a694e509 | |||
| 165927882b | |||
| d9d1aeaef9 | |||
| 57e737b5d7 | |||
| a78b02f22b | |||
| 70ec2928ea | |||
| c618c116ba | |||
| 29021b5a71 | |||
| c0fd6a80e8 | |||
| 7337683c16 | |||
| b36ecc121c | |||
| 42f0fa0e83 | |||
| 41d61bde41 | |||
| ac96fe5292 | |||
| 9b9ca8f707 | |||
| db20387502 | |||
| 44c43c8ebd | |||
| 4b5b27e686 | |||
| 5c823d0e8b | |||
| 1679897089 | |||
| ce8f75411a | |||
| f75da92fa5 | |||
| 9eaaac7cee | |||
| d446779d34 | |||
| 585d11b93b | |||
| 17f9535f0e | |||
| 8bfbb9c7fe | |||
| 2b8fe71a82 | |||
| 29b7f086c9 | |||
| eb3d8d6eb1 | |||
| 787c75b9b3 | |||
| 2a784aa405 | |||
| 2649857cb5 | |||
| c4b4aed2c5 | |||
| 757f6ea4b5 | |||
| df3ee02f8d | |||
| dfe61ec643 | |||
| 52e17b5056 | |||
| c1f3abfd5d | |||
| b5ee2c2cb7 | |||
| 16c1b74636 | |||
| 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 |
+1
-1
@@ -14,7 +14,7 @@ indent_size = 4
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
|
||||
[*.{md,json,yml,props,csproj}]
|
||||
[*.{md,json,yml,xml,props,csproj}]
|
||||
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
|
||||
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: 120 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 310 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
|
||||
|
||||
@@ -26,6 +26,7 @@ arm64/
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
.packages
|
||||
packages/
|
||||
*.nupkg
|
||||
.nuget/
|
||||
@@ -36,3 +37,5 @@ packages/
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
ehthumbs.db
|
||||
|
||||
.vs/
|
||||
|
||||
@@ -1,13 +1,22 @@
|
||||
<!--
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
+3
-1
@@ -3,12 +3,14 @@ version = 1
|
||||
[[annotations]]
|
||||
path = [
|
||||
"REUSE.toml",
|
||||
"nuget.config",
|
||||
"global.json",
|
||||
"**/packages.lock.json",
|
||||
"scripts/ps5_names.txt",
|
||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||
"_logs/**",
|
||||
".github/images/**"
|
||||
".github/images/**",
|
||||
"assets/images/**"
|
||||
]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||
|
||||
@@ -7,6 +7,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
|
||||
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
|
||||
<Project Path="src/SharpEmu.GUI/SharpEmu.GUI.csproj" />
|
||||
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
|
||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.1 MiB |
+1
-1
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.103",
|
||||
"rollForward": "disable"
|
||||
"rollForward": "latestFeature"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<configuration>
|
||||
<config>
|
||||
<add key="globalPackagesFolder" value=".packages" />
|
||||
</config>
|
||||
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="Nuget" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
|
||||
<packageSourceMapping>
|
||||
<packageSource key="Nuget">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
+115
-2
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.Core.Runtime;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.GUI;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -24,12 +25,44 @@ internal static partial class Program
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
private const int ATTACH_PARENT_PROCESS = -1;
|
||||
private const int STD_OUTPUT_HANDLE = -11;
|
||||
private const int STD_ERROR_HANDLE = -12;
|
||||
private const uint GENERIC_READ = 0x80000000;
|
||||
private const uint GENERIC_WRITE = 0x40000000;
|
||||
private const uint FILE_SHARE_READ = 0x00000001;
|
||||
private const uint FILE_SHARE_WRITE = 0x00000002;
|
||||
private const uint OPEN_EXISTING = 3;
|
||||
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Run(args);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SharpEmuLog.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private static int Run(string[] args)
|
||||
{
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
if (args.Length == 0 && !isMitigatedChild)
|
||||
{
|
||||
// No arguments: open the desktop frontend. Any argument selects
|
||||
// the classic CLI behavior below.
|
||||
return GuiLauncher.Run();
|
||||
}
|
||||
|
||||
// The executable uses the GUI subsystem, so CLI mode has to connect
|
||||
// itself to a console before the first write.
|
||||
EnsureCliConsole();
|
||||
|
||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
||||
{
|
||||
return childExitCode;
|
||||
@@ -45,7 +78,7 @@ internal static partial class Program
|
||||
|
||||
ebootPath = Path.GetFullPath(ebootPath);
|
||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||
|
||||
|
||||
if (!File.Exists(ebootPath))
|
||||
{
|
||||
Log.Error($"EBOOT file was not found: {ebootPath}");
|
||||
@@ -101,6 +134,58 @@ internal static partial class Program
|
||||
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
|
||||
}
|
||||
|
||||
private static void EnsureCliConsole()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard handles already provided (pipes or file redirection, e.g.
|
||||
// when the GUI or a script launches us): use them as-is.
|
||||
if (IsHandleValid(GetStdHandle(STD_OUTPUT_HANDLE)) && IsHandleValid(GetStdHandle(STD_ERROR_HANDLE)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer the console of the parent process (interactive terminal);
|
||||
// create one only when started with arguments but no terminal at all
|
||||
// (e.g. a shortcut), so usage and errors remain visible.
|
||||
if (!AttachConsole(ATTACH_PARENT_PROCESS) && GetConsoleWindow() == 0)
|
||||
{
|
||||
_ = AllocConsole();
|
||||
}
|
||||
|
||||
RebindStdHandleToConsole(STD_OUTPUT_HANDLE);
|
||||
RebindStdHandleToConsole(STD_ERROR_HANDLE);
|
||||
}
|
||||
|
||||
private static void RebindStdHandleToConsole(int stdHandle)
|
||||
{
|
||||
if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var conOut = CreateFileW(
|
||||
"CONOUT$",
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
0,
|
||||
OPEN_EXISTING,
|
||||
0,
|
||||
0);
|
||||
if (IsHandleValid(conOut))
|
||||
{
|
||||
_ = SetStdHandle(stdHandle, conOut);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsHandleValid(nint handle)
|
||||
{
|
||||
return handle != 0 && handle != -1;
|
||||
}
|
||||
|
||||
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
||||
{
|
||||
isMitigatedChild = false;
|
||||
@@ -670,4 +755,32 @@ internal static partial class Program
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(nint handle);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern nint GetConsoleWindow();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AttachConsole(int processId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AllocConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint GetStdHandle(int stdHandle);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetStdHandle(int stdHandle, nint handle);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern nint CreateFileW(
|
||||
string fileName,
|
||||
uint desiredAccess,
|
||||
uint shareMode,
|
||||
nint securityAttributes,
|
||||
uint creationDisposition,
|
||||
uint flagsAndAttributes,
|
||||
nint templateFile);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<!-- GUI subsystem so starting without arguments opens the frontend with no
|
||||
console window; CLI mode re-attaches to the parent terminal's console. -->
|
||||
<OutputType>WinExe</OutputType>
|
||||
<AssemblyName>SharpEmu</AssemblyName>
|
||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
|
||||
<SelfContained>true</SelfContained>
|
||||
@@ -30,6 +33,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
|
||||
<ApplicationIcon>..\..\assets\images\SharpEmu.ico</ApplicationIcon>
|
||||
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
@@ -38,4 +50,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||
library is embedded into the single-file bundle. -->
|
||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
||||
<ItemGroup>
|
||||
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
|
||||
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw'))" />
|
||||
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
|
||||
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -4,9 +4,196 @@
|
||||
"net10.0": {
|
||||
"Microsoft.NET.ILLink.Tasks": {
|
||||
"type": "Direct",
|
||||
"requested": "[10.0.3, )",
|
||||
"resolved": "10.0.3",
|
||||
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||
"requested": "[10.0.9, )",
|
||||
"resolved": "10.0.9",
|
||||
"contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg=="
|
||||
},
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.BuildServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.2",
|
||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||
},
|
||||
"Avalonia.FreeDesktop": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Tmds.DBus.Protocol": "0.21.3"
|
||||
}
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Remote.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
||||
},
|
||||
"Avalonia.Skia": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"HarfBuzzSharp": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
||||
"SkiaSharp": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
||||
}
|
||||
},
|
||||
"Avalonia.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
||||
}
|
||||
},
|
||||
"Avalonia.X11": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.FreeDesktop": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
||||
"dependencies": {
|
||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"MicroCom.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.11.0",
|
||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||
},
|
||||
"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"
|
||||
}
|
||||
},
|
||||
"SkiaSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||
"dependencies": {
|
||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.core": {
|
||||
"type": "Project",
|
||||
@@ -17,27 +204,284 @@
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.gui": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Avalonia": "[11.3.18, )",
|
||||
"Avalonia.Desktop": "[11.3.18, )",
|
||||
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
||||
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
||||
"Tmds.DBus.Protocol": "[0.21.3, )"
|
||||
}
|
||||
},
|
||||
"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.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Avalonia": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "11.3.18",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Native": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18",
|
||||
"Avalonia.Win32": "11.3.18",
|
||||
"Avalonia.X11": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Iced": {
|
||||
"type": "CentralTransitive",
|
||||
"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.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "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"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[0.21.3, )",
|
||||
"resolved": "0.21.3",
|
||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||
}
|
||||
},
|
||||
"net10.0/linux-x64": {},
|
||||
"net10.0/osx-arm64": {},
|
||||
"net10.0/win-x64": {}
|
||||
"net10.0/linux-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/osx-arm64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
},
|
||||
"net10.0/win-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu;
|
||||
|
||||
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
|
||||
|
||||
private enum EntryFrameKind
|
||||
{
|
||||
ProcessEntry,
|
||||
@@ -22,6 +25,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;
|
||||
@@ -79,8 +83,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string processImageName = "eboot.bin",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
Log.Debug("=== DispatchEntry START ===");
|
||||
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -88,8 +92,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -102,8 +105,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string moduleName = "module",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
Log.Debug("=== DispatchModuleInitializer START ===");
|
||||
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -118,8 +121,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -133,7 +135,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
CpuExecutionOptions executionOptions = default,
|
||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
||||
Log.Debug("DispatchEntryCore STARTING...");
|
||||
|
||||
LastEntryPoint = entryPoint;
|
||||
LastTrapInfo = null;
|
||||
@@ -305,7 +307,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
LastMilestoneLog,
|
||||
Environment.NewLine,
|
||||
$"CpuEngine native-only failed: {backendError}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
||||
Log.Error($"Native backend FAILED: {backendError}");
|
||||
return FailEarly(
|
||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||
CpuExitReason.NativeBackendUnavailable);
|
||||
@@ -342,11 +344,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 +366,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)
|
||||
@@ -411,8 +416,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
|
||||
const ulong entryParamsSize = 0x20;
|
||||
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
||||
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
|
||||
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
|
||||
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
|
||||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
||||
@@ -420,6 +425,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;
|
||||
@@ -445,13 +457,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
return value & ~(alignment - 1);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return context.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static string BuildEntryFrameDiagnostic(
|
||||
ulong entryPoint,
|
||||
CpuContext context,
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -4,18 +4,28 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
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 static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
||||
|
||||
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||
private readonly object _guestAllocationGate = new();
|
||||
private readonly object _allocationSearchHintGate = new();
|
||||
private readonly List<MemoryRegion> _regions = new();
|
||||
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = 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 FullCommitRegionLimit = 4UL << 30;
|
||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||
|
||||
private const uint MEM_COMMIT = 0x1000;
|
||||
@@ -29,6 +39,10 @@ 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;
|
||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||
|
||||
@@ -44,7 +58,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||
private static extern void* GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||
|
||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||
{
|
||||
@@ -71,9 +89,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
@@ -82,9 +101,13 @@ 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)");
|
||||
TraceVmem($"Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -98,7 +121,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
||||
var reservedOnly = false;
|
||||
var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold;
|
||||
var preferReserveOnly = !executable &&
|
||||
alignedSize >= LargeDataReserveThreshold &&
|
||||
alignedSize > FullCommitRegionLimit;
|
||||
|
||||
void* result = null;
|
||||
if (preferReserveOnly)
|
||||
@@ -127,7 +152,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
throw new InvalidOperationException($"Failed to allocate exact mapping at 0x{desiredAddress:X16} ({alignedSize} bytes)");
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[VMEM] Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
||||
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
||||
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
|
||||
|
||||
if (result == null)
|
||||
@@ -181,12 +206,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
lazyPrimeState = committedBytes == primeBytes
|
||||
? $"ok:{committedBytes:X}"
|
||||
: $"partial:{committedBytes:X}/{primeBytes:X}";
|
||||
Console.Error.WriteLine($"[VMEM] Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
||||
TraceVmem($"Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
||||
}
|
||||
else
|
||||
{
|
||||
lazyPrimeState = $"fail:{primeBytes:X}";
|
||||
Console.Error.WriteLine($"[VMEM] Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
||||
TraceVmem($"Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -195,9 +220,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
}
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
@@ -206,25 +232,135 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
Protection = protection
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
var allocationKind = reservedOnly
|
||||
? "reserved data memory (lazy commit)"
|
||||
: (executable ? "executable memory" : "data memory");
|
||||
Console.Error.WriteLine($"[VMEM] Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
|
||||
TraceVmem($"Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
|
||||
|
||||
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 requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||
|
||||
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)
|
||||
{
|
||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
||||
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();
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
_allocationSearchHints.Clear();
|
||||
}
|
||||
}
|
||||
_regions.Clear();
|
||||
_pageProtections.Clear();
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
_guestAllocationArenaBase = 0;
|
||||
_guestAllocationOffset = 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -241,7 +377,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)
|
||||
@@ -272,7 +409,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
|
||||
ApplySegmentProtection(mapStart, mapEnd, protection);
|
||||
|
||||
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
||||
TraceVmem($"Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -317,13 +458,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
|
||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||
{
|
||||
FlushInstructionCache(null, (void*)address, (nuint)size);
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
||||
for (var i = 0; i < _regions.Count; i++)
|
||||
@@ -338,96 +480,240 @@ 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)
|
||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)destination.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
if (TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
RestorePageProtections(touchedPages);
|
||||
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)
|
||||
var region = FindRegion(virtualAddress, (ulong)source.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)source.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
if (TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (source.IsEmpty)
|
||||
{
|
||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (source.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
||||
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (byte* srcPtr = source)
|
||||
{
|
||||
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);
|
||||
}
|
||||
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)
|
||||
{
|
||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)destination.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
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)
|
||||
{
|
||||
var region = FindRegion(virtualAddress, (ulong)source.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)source.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
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(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
|
||||
@@ -439,45 +725,146 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
|
||||
public void* GetPointer(ulong virtualAddress)
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (virtualAddress >= region.VirtualAddress &&
|
||||
virtualAddress < region.VirtualAddress + region.Size)
|
||||
{
|
||||
return (void*)virtualAddress;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return FindRegion(virtualAddress, 1) is not null
|
||||
? (void*)virtualAddress
|
||||
: null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAccessible(ulong virtualAddress, ulong size)
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (TryResolveRegionOffset(virtualAddress, size, region, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return FindRegion(virtualAddress, size) is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
private MemoryRegion? FindRegion(ulong address, ulong size)
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
var low = 0;
|
||||
var high = _regions.Count - 1;
|
||||
MemoryRegion? candidate = null;
|
||||
while (low <= high)
|
||||
{
|
||||
if (TryResolveRegionOffset(address, size, region, out _))
|
||||
var middle = low + ((high - low) >> 1);
|
||||
var region = _regions[middle];
|
||||
if (region.VirtualAddress <= address)
|
||||
{
|
||||
return region;
|
||||
candidate = region;
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
return candidate is not null &&
|
||||
TryResolveRegionOffset(address, size, candidate, out _)
|
||||
? candidate
|
||||
: null;
|
||||
}
|
||||
|
||||
private void InsertRegionSorted(MemoryRegion region)
|
||||
{
|
||||
var low = 0;
|
||||
var high = _regions.Count;
|
||||
while (low < high)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (_regions[middle].VirtualAddress < region.VirtualAddress)
|
||||
{
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
|
||||
_regions.Insert(low, region);
|
||||
}
|
||||
|
||||
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 (region.VirtualAddress >= end)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (regionEnd <= address)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address < regionEnd && region.VirtualAddress < end)
|
||||
{
|
||||
overlapEnd = Math.Max(overlapEnd, regionEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
|
||||
return overlapEnd != 0;
|
||||
}
|
||||
|
||||
private ulong GetAllocationSearchCursor(
|
||||
ulong desiredAddress,
|
||||
ulong requestedCursor,
|
||||
ulong alignment,
|
||||
bool executable)
|
||||
{
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
var key = (desiredAddress, alignment, executable);
|
||||
if (_allocationSearchHints.TryGetValue(key, out var hintedCursor) &&
|
||||
hintedCursor > requestedCursor)
|
||||
{
|
||||
return AlignUp(hintedCursor, alignment);
|
||||
}
|
||||
}
|
||||
|
||||
return requestedCursor;
|
||||
}
|
||||
|
||||
private void UpdateAllocationSearchCursor(
|
||||
ulong desiredAddress,
|
||||
ulong alignment,
|
||||
bool executable,
|
||||
ulong nextCursor)
|
||||
{
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
_allocationSearchHints[(desiredAddress, alignment, executable)] = AlignUp(nextCursor, alignment);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
|
||||
@@ -507,6 +894,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 +948,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 +976,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;
|
||||
@@ -596,6 +1035,29 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IDisposable
|
||||
return checked((value + mask) & ~mask);
|
||||
}
|
||||
|
||||
private static ulong ResolveLazyReservePrimeBytes()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_LAZY_RESERVE_PRIME_MB");
|
||||
if (ulong.TryParse(configured, out var megabytes))
|
||||
{
|
||||
return megabytes == 0
|
||||
? 0
|
||||
: checked(Math.Min(megabytes, 4096UL) * 1024UL * 1024UL);
|
||||
}
|
||||
|
||||
return DefaultLazyReservePrimeBytes;
|
||||
}
|
||||
|
||||
private static void TraceVmem(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Debug(message);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
|
||||
@@ -10,6 +10,8 @@ using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.AppContent;
|
||||
using SharpEmu.Libs.SaveData;
|
||||
using SharpEmu.Logging;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
@@ -19,6 +21,8 @@ namespace SharpEmu.Core.Runtime;
|
||||
|
||||
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
|
||||
|
||||
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
||||
|
||||
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -127,7 +131,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||
Log.Info($"Loading: {ebootPath}");
|
||||
LastExecutionDiagnostics = null;
|
||||
LastExecutionTrace = null;
|
||||
LastSessionSummary = null;
|
||||
@@ -135,9 +139,11 @@ 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}");
|
||||
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
||||
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
||||
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
||||
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
||||
@@ -160,7 +166,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName);
|
||||
if (initializerResult is { } failedInitializerResult)
|
||||
{
|
||||
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
||||
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -168,8 +174,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return failedInitializerResult;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
||||
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
Log.Info($"Dispatching, gen: {generation}");
|
||||
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
image.EntryPoint,
|
||||
@@ -179,8 +185,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName,
|
||||
_cpuExecutionOptions);
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||
Log.Info($"DispatchEntry returned: {result}");
|
||||
Log.Info($"Dispatch result: {result}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -431,8 +437,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
moduleName = $"module#{i}";
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
Log.Info(
|
||||
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||
initEntryPoint,
|
||||
@@ -443,8 +449,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
_cpuExecutionOptions);
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
||||
Log.Error(
|
||||
$"Module start failed: {moduleName} -> {result}");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -465,8 +471,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return null;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
Log.Info(
|
||||
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
|
||||
var result = RunInitializerList(
|
||||
$"{label}:preinit",
|
||||
@@ -505,8 +511,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
Log.Debug(
|
||||
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
initializerAddress,
|
||||
@@ -540,6 +546,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
Path.Combine(ebootDirectory, "sce_module"),
|
||||
Path.Combine(ebootDirectory, "sce_modules"),
|
||||
Path.Combine(ebootDirectory, "Media", "Modules"),
|
||||
}
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Where(Directory.Exists)
|
||||
@@ -551,7 +558,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
}
|
||||
|
||||
var allModulePaths = moduleDirectories
|
||||
.SelectMany(Directory.EnumerateFiles)
|
||||
.SelectMany(directory => Directory
|
||||
.EnumerateFiles(directory)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
||||
.Where(path =>
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
@@ -559,7 +568,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
||||
})
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var modulePaths = allModulePaths
|
||||
@@ -572,7 +580,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
.ToArray();
|
||||
if (skippedModules.Length > 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||
}
|
||||
|
||||
if (modulePaths.Length == 0)
|
||||
@@ -580,8 +588,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
||||
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
||||
var loadedModules = 0;
|
||||
var failedModules = 0;
|
||||
var mergedImportCount = 0;
|
||||
@@ -616,18 +624,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
||||
loadedModules++;
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
Log.Info(
|
||||
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failedModules++;
|
||||
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
|
||||
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
Log.Info(
|
||||
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
@@ -647,8 +655,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (rebound != 0 || unresolved != 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
Log.Info(
|
||||
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -680,8 +688,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
Log.Warning(
|
||||
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -693,8 +701,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Log.Error(
|
||||
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -703,8 +711,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Log.Debug(
|
||||
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
rebound++;
|
||||
@@ -740,8 +748,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
Log.Warning(
|
||||
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -850,8 +858,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
image.EntryPoint,
|
||||
isMain,
|
||||
isSystemModule);
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||
Log.Info(
|
||||
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||
}
|
||||
|
||||
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
@@ -14,6 +14,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageReference Include="Iced" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -8,17 +8,116 @@
|
||||
"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.EXT": "[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.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,198 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="SharpEmu.GUI.App"
|
||||
RequestedThemeVariant="Dark">
|
||||
|
||||
<Application.Resources>
|
||||
<Color x:Key="SystemAccentColor">#7C5CFC</Color>
|
||||
|
||||
<LinearGradientBrush x:Key="BgBrush" StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0" Color="#12151F" />
|
||||
<GradientStop Offset="0.55" Color="#0D1017" />
|
||||
<GradientStop Offset="1" Color="#0B0D14" />
|
||||
</LinearGradientBrush>
|
||||
|
||||
<SolidColorBrush x:Key="ChromeBrush" Color="#090C12" />
|
||||
<SolidColorBrush x:Key="CardBrush" Color="#141924" />
|
||||
<SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" />
|
||||
<SolidColorBrush x:Key="ElevatedBrush" Color="#1B2230" />
|
||||
<SolidColorBrush x:Key="TextBrush" Color="#E8ECF4" />
|
||||
<SolidColorBrush x:Key="MutedBrush" Color="#8B94A7" />
|
||||
<SolidColorBrush x:Key="FaintBrush" Color="#5A6478" />
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="#7C5CFC" />
|
||||
<SolidColorBrush x:Key="AccentHoverBrush" Color="#8F73FF" />
|
||||
<SolidColorBrush x:Key="DangerBrush" Color="#E5484D" />
|
||||
<SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" />
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
|
||||
<SolidColorBrush x:Key="TileHoverBrush" Color="#1A2130" />
|
||||
<SolidColorBrush x:Key="TileSelectedBrush" Color="#212A3F" />
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
|
||||
<Style Selector="Window">
|
||||
<Setter Property="FontFamily" Value="Inter, Segoe UI, sans-serif" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.card">
|
||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="Padding" Value="16" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.pill">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="CornerRadius" Value="999" />
|
||||
<Setter Property="Padding" Value="10,3" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.sectionTitle">
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="LetterSpacing" Value="1.5" />
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.fieldLabel">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
|
||||
<Setter Property="Margin" Value="0,0,0,6" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.accent">
|
||||
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Padding" Value="22,10" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource AccentHoverBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.danger">
|
||||
<Setter Property="Background" Value="{StaticResource DangerBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Padding" Value="22,10" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.danger:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource DangerHoverBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.ghost">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="Padding" Value="12,7" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ToggleButton.ghost">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="Padding" Value="12,7" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="ToggleButton.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="ToggleButton.ghost:checked /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ContextMenu">
|
||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="Padding" Value="6" />
|
||||
</Style>
|
||||
<Style Selector="ContextMenu MenuItem">
|
||||
<Setter Property="Padding" Value="10,7" />
|
||||
<Setter Property="CornerRadius" Value="7" />
|
||||
</Style>
|
||||
<Style Selector="ContextMenu Separator">
|
||||
<Setter Property="Background" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="Height" Value="1" />
|
||||
<Setter Property="Margin" Value="8,4" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ListBox.console">
|
||||
<Setter Property="Background" Value="#0B0E14" />
|
||||
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.console ListBoxItem">
|
||||
<Setter Property="Padding" Value="10,1" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
</Style>
|
||||
|
||||
<!-- Cover-art library grid -->
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem">
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<Setter Property="Margin" Value="5" />
|
||||
<Setter Property="CornerRadius" Value="14" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="RenderTransform" Value="translateY(0px)" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" />
|
||||
</Transitions>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover">
|
||||
<Setter Property="RenderTransform" Value="translateY(-3px)" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource TileHoverBrush}" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentHoverBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.coverShadow">
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="BoxShadow" Value="0 6 14 0 #55000000" />
|
||||
</Style>
|
||||
<Style Selector="Border.coverClip">
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="ClipToBounds" Value="True" />
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
</Application>
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow();
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,645 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Launches the SharpEmu CLI as a child process with the same CET/CFG mitigation
|
||||
/// opt-outs the CLI would apply to its own relaunched child, while capturing
|
||||
/// stdout/stderr through pipes. The CLI's internal relaunch is suppressed via
|
||||
/// SHARPEMU_DISABLE_MITIGATION_RELAUNCH so output is not lost to a detached
|
||||
/// console. A kill-on-close job object ties the emulator's lifetime to the GUI.
|
||||
/// </summary>
|
||||
internal sealed class EmulatorProcess : IDisposable
|
||||
{
|
||||
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
|
||||
private const uint CREATE_NO_WINDOW = 0x08000000;
|
||||
private const int STARTF_USESTDHANDLES = 0x00000100;
|
||||
private const uint HANDLE_FLAG_INHERIT = 0x00000001;
|
||||
private const uint INFINITE = 0xFFFFFFFF;
|
||||
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
|
||||
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
||||
private const int JobObjectExtendedLimitInformation = 9;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
|
||||
private readonly object _sync = new();
|
||||
private nint _processHandle;
|
||||
private nint _jobHandle;
|
||||
private Process? _fallbackProcess;
|
||||
private bool _running;
|
||||
private bool _disposed;
|
||||
|
||||
public event Action<string, bool>? OutputReceived;
|
||||
|
||||
public event Action<int>? Exited;
|
||||
|
||||
public bool IsRunning
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _running;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Start(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_running)
|
||||
{
|
||||
throw new InvalidOperationException("The emulator process is already running.");
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
StartWindows(exePath, arguments, workingDirectory);
|
||||
}
|
||||
else
|
||||
{
|
||||
StartFallback(exePath, arguments, workingDirectory);
|
||||
}
|
||||
|
||||
_running = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_processHandle != 0)
|
||||
{
|
||||
_ = TerminateProcess(_processHandle, 1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_fallbackProcess?.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Already exited.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
Stop();
|
||||
}
|
||||
|
||||
private void StartWindows(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
|
||||
{
|
||||
// The CLI would otherwise relaunch itself into a mitigated child whose
|
||||
// console output cannot flow through our pipes.
|
||||
Environment.SetEnvironmentVariable("SHARPEMU_DISABLE_MITIGATION_RELAUNCH", "1");
|
||||
|
||||
var securityAttributes = new SECURITY_ATTRIBUTES
|
||||
{
|
||||
nLength = Marshal.SizeOf<SECURITY_ATTRIBUTES>(),
|
||||
bInheritHandle = 1,
|
||||
};
|
||||
|
||||
if (!CreatePipe(out var stdoutRead, out var stdoutWrite, ref securityAttributes, 0) ||
|
||||
!CreatePipe(out var stderrRead, out var stderrWrite, ref securityAttributes, 0))
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create output pipes.");
|
||||
}
|
||||
|
||||
_ = SetHandleInformation(stdoutRead, HANDLE_FLAG_INHERIT, 0);
|
||||
_ = SetHandleInformation(stderrRead, HANDLE_FLAG_INHERIT, 0);
|
||||
|
||||
var startupInfoEx = new STARTUPINFOEX();
|
||||
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
|
||||
startupInfoEx.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
startupInfoEx.StartupInfo.hStdOutput = stdoutWrite;
|
||||
startupInfoEx.StartupInfo.hStdError = stderrWrite;
|
||||
|
||||
nint attributeList = 0;
|
||||
nint mitigationPolicies = 0;
|
||||
try
|
||||
{
|
||||
nuint attributeListSize = 0;
|
||||
_ = InitializeProcThreadAttributeList(0, 1, 0, ref attributeListSize);
|
||||
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
|
||||
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to initialize the process attribute list.");
|
||||
}
|
||||
|
||||
startupInfoEx.lpAttributeList = attributeList;
|
||||
|
||||
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
var policy2 =
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
|
||||
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
||||
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
||||
Marshal.WriteInt64(nint.Add(mitigationPolicies, sizeof(long)), unchecked((long)policy2));
|
||||
|
||||
if (!UpdateProcThreadAttribute(
|
||||
attributeList,
|
||||
0,
|
||||
PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
|
||||
mitigationPolicies,
|
||||
(nuint)(sizeof(ulong) * 2),
|
||||
0,
|
||||
0))
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to apply the mitigation policy.");
|
||||
}
|
||||
|
||||
var currentDirectory = workingDirectory ?? Environment.CurrentDirectory;
|
||||
var created = CreateProcessW(
|
||||
exePath,
|
||||
new StringBuilder(BuildCommandLine(exePath, arguments)),
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
|
||||
0,
|
||||
currentDirectory,
|
||||
ref startupInfoEx,
|
||||
out var processInfo);
|
||||
|
||||
if (!created)
|
||||
{
|
||||
// Some mitigation policy bits (e.g. XFG) are not supported on
|
||||
// older Windows builds. Mirror the CLI's behavior and fall back
|
||||
// to launching without the mitigation attribute list.
|
||||
startupInfoEx.lpAttributeList = 0;
|
||||
created = CreateProcessW(
|
||||
exePath,
|
||||
new StringBuilder(BuildCommandLine(exePath, arguments)),
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
CREATE_NO_WINDOW,
|
||||
0,
|
||||
currentDirectory,
|
||||
ref startupInfoEx,
|
||||
out processInfo);
|
||||
}
|
||||
|
||||
if (!created)
|
||||
{
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
throw new Win32Exception(error, $"Failed to start '{exePath}' (Win32 error {error}: {new Win32Exception(error).Message}).");
|
||||
}
|
||||
|
||||
CloseHandle(processInfo.hThread);
|
||||
_processHandle = processInfo.hProcess;
|
||||
|
||||
_jobHandle = CreateJobObjectW(0, null);
|
||||
if (_jobHandle != 0 &&
|
||||
(!TryEnableKillOnJobClose(_jobHandle) || !AssignProcessToJobObject(_jobHandle, processInfo.hProcess)))
|
||||
{
|
||||
CloseHandle(_jobHandle);
|
||||
_jobHandle = 0;
|
||||
}
|
||||
|
||||
StartReaderThread(stdoutRead, isError: false);
|
||||
StartReaderThread(stderrRead, isError: true);
|
||||
StartExitWatcherThread();
|
||||
}
|
||||
catch
|
||||
{
|
||||
CloseHandle(stdoutRead);
|
||||
CloseHandle(stderrRead);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The child owns duplicated pipe write ends; closing ours lets the
|
||||
// readers observe EOF when the child exits.
|
||||
CloseHandle(stdoutWrite);
|
||||
CloseHandle(stderrWrite);
|
||||
|
||||
if (attributeList != 0)
|
||||
{
|
||||
DeleteProcThreadAttributeList(attributeList);
|
||||
Marshal.FreeHGlobal(attributeList);
|
||||
}
|
||||
|
||||
if (mitigationPolicies != 0)
|
||||
{
|
||||
Marshal.FreeHGlobal(mitigationPolicies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartFallback(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
|
||||
process.OutputDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data is not null)
|
||||
{
|
||||
OutputReceived?.Invoke(e.Data, false);
|
||||
}
|
||||
};
|
||||
process.ErrorDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data is not null)
|
||||
{
|
||||
OutputReceived?.Invoke(e.Data, true);
|
||||
}
|
||||
};
|
||||
process.Exited += (_, _) =>
|
||||
{
|
||||
int exitCode;
|
||||
try
|
||||
{
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
exitCode = -1;
|
||||
}
|
||||
|
||||
OnExited(exitCode);
|
||||
};
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
_fallbackProcess = process;
|
||||
}
|
||||
|
||||
private void StartReaderThread(nint readHandle, bool isError)
|
||||
{
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
using var stream = new FileStream(new SafeFileHandle(readHandle, ownsHandle: true), FileAccess.Read);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
try
|
||||
{
|
||||
while (reader.ReadLine() is { } line)
|
||||
{
|
||||
OutputReceived?.Invoke(line, isError);
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Pipe broken on process teardown.
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = isError ? "SharpEmu stderr reader" : "SharpEmu stdout reader",
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private void StartExitWatcherThread()
|
||||
{
|
||||
var processHandle = _processHandle;
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
_ = WaitForSingleObject(processHandle, INFINITE);
|
||||
var exitCode = GetExitCodeProcess(processHandle, out var rawExitCode)
|
||||
? unchecked((int)rawExitCode)
|
||||
: -1;
|
||||
OnExited(exitCode);
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SharpEmu exit watcher",
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private void OnExited(int exitCode)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_running = false;
|
||||
if (_processHandle != 0)
|
||||
{
|
||||
CloseHandle(_processHandle);
|
||||
_processHandle = 0;
|
||||
}
|
||||
|
||||
if (_jobHandle != 0)
|
||||
{
|
||||
CloseHandle(_jobHandle);
|
||||
_jobHandle = 0;
|
||||
}
|
||||
|
||||
_fallbackProcess?.Dispose();
|
||||
_fallbackProcess = null;
|
||||
}
|
||||
|
||||
Exited?.Invoke(exitCode);
|
||||
}
|
||||
|
||||
private static bool TryEnableKillOnJobClose(nint jobHandle)
|
||||
{
|
||||
var extendedLimitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||
{
|
||||
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
|
||||
{
|
||||
LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
},
|
||||
};
|
||||
|
||||
var size = Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>();
|
||||
var memory = Marshal.AllocHGlobal(size);
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(extendedLimitInfo, memory, false);
|
||||
return SetInformationJobObject(
|
||||
jobHandle,
|
||||
JobObjectExtendedLimitInformation,
|
||||
memory,
|
||||
unchecked((uint)size));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(memory);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(QuoteArgument(processPath));
|
||||
for (var i = 0; i < args.Count; i++)
|
||||
{
|
||||
builder.Append(' ');
|
||||
builder.Append(QuoteArgument(args[i]));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string QuoteArgument(string argument)
|
||||
{
|
||||
if (argument.Length == 0)
|
||||
{
|
||||
return "\"\"";
|
||||
}
|
||||
|
||||
var needsQuotes = false;
|
||||
foreach (var c in argument)
|
||||
{
|
||||
if (char.IsWhiteSpace(c) || c == '"')
|
||||
{
|
||||
needsQuotes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsQuotes)
|
||||
{
|
||||
return argument;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(argument.Length + 2);
|
||||
builder.Append('"');
|
||||
|
||||
var backslashCount = 0;
|
||||
foreach (var c in argument)
|
||||
{
|
||||
if (c == '\\')
|
||||
{
|
||||
backslashCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"')
|
||||
{
|
||||
builder.Append('\\', (backslashCount * 2) + 1);
|
||||
builder.Append('"');
|
||||
backslashCount = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (backslashCount > 0)
|
||||
{
|
||||
builder.Append('\\', backslashCount);
|
||||
backslashCount = 0;
|
||||
}
|
||||
|
||||
builder.Append(c);
|
||||
}
|
||||
|
||||
if (backslashCount > 0)
|
||||
{
|
||||
builder.Append('\\', backslashCount * 2);
|
||||
}
|
||||
|
||||
builder.Append('"');
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct SECURITY_ATTRIBUTES
|
||||
{
|
||||
public int nLength;
|
||||
public nint lpSecurityDescriptor;
|
||||
public int bInheritHandle;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct STARTUPINFO
|
||||
{
|
||||
public int cb;
|
||||
public nint lpReserved;
|
||||
public nint lpDesktop;
|
||||
public nint lpTitle;
|
||||
public int dwX;
|
||||
public int dwY;
|
||||
public int dwXSize;
|
||||
public int dwYSize;
|
||||
public int dwXCountChars;
|
||||
public int dwYCountChars;
|
||||
public int dwFillAttribute;
|
||||
public int dwFlags;
|
||||
public short wShowWindow;
|
||||
public short cbReserved2;
|
||||
public nint lpReserved2;
|
||||
public nint hStdInput;
|
||||
public nint hStdOutput;
|
||||
public nint hStdError;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct STARTUPINFOEX
|
||||
{
|
||||
public STARTUPINFO StartupInfo;
|
||||
public nint lpAttributeList;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct PROCESS_INFORMATION
|
||||
{
|
||||
public nint hProcess;
|
||||
public nint hThread;
|
||||
public int dwProcessId;
|
||||
public int dwThreadId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
|
||||
{
|
||||
public long PerProcessUserTimeLimit;
|
||||
public long PerJobUserTimeLimit;
|
||||
public uint LimitFlags;
|
||||
public nuint MinimumWorkingSetSize;
|
||||
public nuint MaximumWorkingSetSize;
|
||||
public uint ActiveProcessLimit;
|
||||
public nint Affinity;
|
||||
public uint PriorityClass;
|
||||
public uint SchedulingClass;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IO_COUNTERS
|
||||
{
|
||||
public ulong ReadOperationCount;
|
||||
public ulong WriteOperationCount;
|
||||
public ulong OtherOperationCount;
|
||||
public ulong ReadTransferCount;
|
||||
public ulong WriteTransferCount;
|
||||
public ulong OtherTransferCount;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||
{
|
||||
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
||||
public IO_COUNTERS IoInfo;
|
||||
public nuint ProcessMemoryLimit;
|
||||
public nuint JobMemoryLimit;
|
||||
public nuint PeakProcessMemoryUsed;
|
||||
public nuint PeakJobMemoryUsed;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CreatePipe(
|
||||
out nint hReadPipe,
|
||||
out nint hWritePipe,
|
||||
ref SECURITY_ATTRIBUTES lpPipeAttributes,
|
||||
uint nSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetHandleInformation(nint hObject, uint dwMask, uint dwFlags);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool InitializeProcThreadAttributeList(
|
||||
nint lpAttributeList,
|
||||
int dwAttributeCount,
|
||||
int dwFlags,
|
||||
ref nuint lpSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool UpdateProcThreadAttribute(
|
||||
nint lpAttributeList,
|
||||
uint dwFlags,
|
||||
nint attribute,
|
||||
nint lpValue,
|
||||
nuint cbSize,
|
||||
nint lpPreviousValue,
|
||||
nint lpReturnSize);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern void DeleteProcThreadAttributeList(nint lpAttributeList);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "CreateJobObjectW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern nint CreateJobObjectW(nint lpJobAttributes, string? lpName);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetInformationJobObject(
|
||||
nint hJob,
|
||||
int jobObjectInfoClass,
|
||||
nint lpJobObjectInfo,
|
||||
uint cbJobObjectInfoLength);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AssignProcessToJobObject(nint hJob, nint hProcess);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CreateProcessW(
|
||||
string applicationName,
|
||||
StringBuilder commandLine,
|
||||
nint processAttributes,
|
||||
nint threadAttributes,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool inheritHandles,
|
||||
uint creationFlags,
|
||||
nint environment,
|
||||
string currentDirectory,
|
||||
ref STARTUPINFOEX startupInfo,
|
||||
out PROCESS_INFORMATION processInformation);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetExitCodeProcess(nint process, out uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool TerminateProcess(nint process, uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(nint handle);
|
||||
}
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public sealed class GameEntry : INotifyPropertyChanged
|
||||
{
|
||||
// Placeholder gradients for games without cover art, picked
|
||||
// deterministically from the game name so a game keeps its color.
|
||||
private static readonly (Color Start, Color End)[] PlaceholderPalette =
|
||||
{
|
||||
(Color.Parse("#5B4B8A"), Color.Parse("#2C2A4A")),
|
||||
(Color.Parse("#1F6E8C"), Color.Parse("#173B45")),
|
||||
(Color.Parse("#7A4069"), Color.Parse("#3B1C32")),
|
||||
(Color.Parse("#2D6A4F"), Color.Parse("#1B3A2B")),
|
||||
(Color.Parse("#8C5425"), Color.Parse("#4A2B12")),
|
||||
(Color.Parse("#4F6D9E"), Color.Parse("#263349")),
|
||||
(Color.Parse("#8A4B4B"), Color.Parse("#3F2222")),
|
||||
(Color.Parse("#3E7C7B"), Color.Parse("#1E3D3C")),
|
||||
};
|
||||
|
||||
private Bitmap? _cover;
|
||||
private IBrush? _placeholderBrush;
|
||||
private long _sizeBytes;
|
||||
|
||||
public GameEntry(
|
||||
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
|
||||
{
|
||||
Name = name;
|
||||
TitleId = titleId;
|
||||
Path = path;
|
||||
_sizeBytes = sizeBytes;
|
||||
CoverPath = coverPath;
|
||||
BackgroundPath = backgroundPath;
|
||||
Initials = ComputeInitials(name);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string? TitleId { get; }
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Total size of the game. Initially the eboot's own size from the scan;
|
||||
/// replaced with the full install folder size once computed in the
|
||||
/// background.
|
||||
/// </summary>
|
||||
public long SizeBytes
|
||||
{
|
||||
get => _sizeBytes;
|
||||
set
|
||||
{
|
||||
if (_sizeBytes == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_sizeBytes = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Path to the cover art image shipped with the game, if found.</summary>
|
||||
public string? CoverPath { get; }
|
||||
|
||||
/// <summary>Path to the key art (pic0/pic1) shipped with the game, if found.</summary>
|
||||
public string? BackgroundPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Decoded key art used as the window backdrop while this game is
|
||||
/// selected. Loaded on demand and cached; not exposed via binding.
|
||||
/// </summary>
|
||||
public Bitmap? Background { get; set; }
|
||||
|
||||
public string Initials { get; }
|
||||
|
||||
// Built lazily: brushes are AvaloniaObjects that must be created on the
|
||||
// UI thread, while GameEntry itself is constructed on the scan thread.
|
||||
public IBrush PlaceholderBrush => _placeholderBrush ??= BuildPlaceholderBrush(Name);
|
||||
|
||||
/// <summary>Decoded cover art; loaded asynchronously after the library scan.</summary>
|
||||
public Bitmap? Cover
|
||||
{
|
||||
get => _cover;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(_cover, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cover = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Cover)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasCover)));
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasCover => _cover is not null;
|
||||
|
||||
public string Detail => TitleId is not null
|
||||
? $"{TitleId} • {FormatSize(SizeBytes)}"
|
||||
: FormatSize(SizeBytes);
|
||||
|
||||
private static string ComputeInitials(string name)
|
||||
{
|
||||
var initials = name
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(word => char.IsLetterOrDigit(word[0]))
|
||||
.Select(word => char.ToUpperInvariant(word[0]))
|
||||
.Take(2)
|
||||
.ToArray();
|
||||
|
||||
return initials.Length > 0 ? new string(initials) : "?";
|
||||
}
|
||||
|
||||
private static IBrush BuildPlaceholderBrush(string name)
|
||||
{
|
||||
var hash = 0;
|
||||
foreach (var ch in name)
|
||||
{
|
||||
hash = unchecked(hash * 31 + ch);
|
||||
}
|
||||
|
||||
var (start, end) = PlaceholderPalette[(int)((uint)hash % PlaceholderPalette.Length)];
|
||||
return new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops =
|
||||
{
|
||||
new GradientStop(start, 0),
|
||||
new GradientStop(end, 1),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatSize(long bytes)
|
||||
{
|
||||
return bytes switch
|
||||
{
|
||||
>= 1L << 30 => $"{bytes / (double)(1L << 30):0.0} GiB",
|
||||
>= 1L << 20 => $"{bytes / (double)(1L << 20):0.0} MiB",
|
||||
>= 1L << 10 => $"{bytes / (double)(1L << 10):0.0} KiB",
|
||||
_ => $"{bytes} B",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Avalonia;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Entry point for the desktop frontend, hosted by the SharpEmu executable
|
||||
/// when it is started without command-line arguments.
|
||||
/// </summary>
|
||||
public static class GuiLauncher
|
||||
{
|
||||
public static int Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(Array.Empty<string>());
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteCrashLog(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
|
||||
private static void WriteCrashLog(Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.AppendAllText(
|
||||
Path.Combine(AppContext.BaseDirectory, "gui-crash.log"),
|
||||
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {ex}{Environment.NewLine}{Environment.NewLine}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Crash logging is best-effort.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public sealed class GuiSettings
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
public List<string> GameFolders { get; set; } = new();
|
||||
|
||||
/// <summary>Eboot paths hidden from the library via "Remove from library".</summary>
|
||||
public List<string> ExcludedGames { get; set; } = new();
|
||||
|
||||
public string LogLevel { get; set; } = "Info";
|
||||
|
||||
public int ImportTraceLimit { get; set; }
|
||||
|
||||
public bool StrictDynlibResolution { get; set; }
|
||||
|
||||
public string? EmulatorPath { get; set; }
|
||||
|
||||
// The emulator is portable and keeps its data next to the executable;
|
||||
// the GUI follows the same convention.
|
||||
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "gui-settings.json");
|
||||
|
||||
public static GuiSettings Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(SettingsPath))
|
||||
{
|
||||
var json = File.ReadAllText(SettingsPath);
|
||||
return JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Corrupt or unreadable settings fall back to defaults.
|
||||
}
|
||||
|
||||
return new GuiSettings();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(SettingsPath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
File.WriteAllText(SettingsPath, JsonSerializer.Serialize(this, SerializerOptions));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Settings persistence is best-effort.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public sealed record LogLine(string Text, IBrush Brush);
|
||||
@@ -0,0 +1,264 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="SharpEmu.GUI.MainWindow"
|
||||
Title="SharpEmu"
|
||||
Width="1280" Height="820"
|
||||
MinWidth="980" MinHeight="640"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="{StaticResource BgBrush}"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="PreferSystemChrome"
|
||||
ExtendClientAreaTitleBarHeightHint="44"
|
||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico">
|
||||
|
||||
<Grid RowDefinitions="44,*,32">
|
||||
|
||||
<!-- Selected-game backdrop: key art behind the main content, dimmed by
|
||||
a scrim so tiles and text stay readable. Fades on selection. -->
|
||||
<Panel Grid.Row="1" ClipToBounds="True">
|
||||
<Image x:Name="BackdropImage" Stretch="UniformToFill" Opacity="0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Image.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Opacity" Duration="0:0:0.35" />
|
||||
</Transitions>
|
||||
</Image.Transitions>
|
||||
</Image>
|
||||
<Border>
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
|
||||
<GradientStop Offset="0" Color="#730D1017" />
|
||||
<GradientStop Offset="0.55" Color="#BF0D1017" />
|
||||
<GradientStop Offset="1" Color="#EB0D1017" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- Title bar -->
|
||||
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/logo.png" Width="20" Height="20" />
|
||||
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
<Border Classes="pill">
|
||||
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Main content -->
|
||||
<Grid Grid.Row="1" Margin="18,14,18,14" RowDefinitions="Auto,*,Auto,Auto">
|
||||
|
||||
<!-- Library toolbar -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
|
||||
<TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="4" x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="5" x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="6" x:Name="OpenFileButton" Classes="ghost" Content="Open file…"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!-- Cover-art grid -->
|
||||
<Panel Grid.Row="1">
|
||||
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
|
||||
SelectionMode="Single" Padding="0">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
||||
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
||||
Foreground="{StaticResource DangerHoverBrush}">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Width="128" Height="186" Spacing="7">
|
||||
<Border Classes="coverShadow" Width="128" Height="128">
|
||||
<Border Classes="coverClip">
|
||||
<Panel>
|
||||
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
|
||||
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
|
||||
Foreground="#E8ECF4" Opacity="0.85"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
</Border>
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="False">
|
||||
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
|
||||
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
||||
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
||||
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
<!-- Console (collapsible) -->
|
||||
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
|
||||
Margin="0,12,0,0" IsVisible="False">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,12,16,8">
|
||||
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
||||
FontSize="12" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
||||
Padding="10,4" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
||||
Padding="10,4" />
|
||||
</Grid>
|
||||
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
||||
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,12,12">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Launch bar -->
|
||||
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- Selected game cover thumbnail -->
|
||||
<Border Grid.Column="0" Classes="coverClip" Width="56" Height="56" CornerRadius="8"
|
||||
VerticalAlignment="Center">
|
||||
<Panel x:Name="SelectedCoverPanel">
|
||||
<Border Background="{Binding PlaceholderBrush, FallbackValue={x:Null}}"
|
||||
IsVisible="{Binding !HasCover, FallbackValue=False}">
|
||||
<TextBlock Text="{Binding Initials}" FontSize="20" FontWeight="Bold"
|
||||
Foreground="#E8ECF4" Opacity="0.85"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Image Source="{Binding Cover}" Stretch="UniformToFill"
|
||||
IsVisible="{Binding HasCover, FallbackValue=False}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="3" VerticalAlignment="Center" Margin="14,0,14,0">
|
||||
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="16" FontWeight="Bold"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
<Ellipse x:Name="StatusDot" Width="8" Height="8" Fill="{StaticResource FaintBrush}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="StatusText" Text="Idle" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<ToggleButton x:Name="OptionsToggle" Classes="ghost" Content="⚙ Options" />
|
||||
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
|
||||
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
||||
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Launch options (collapsible) -->
|
||||
<WrapPanel x:Name="OptionsPanel" IsVisible="False">
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="CPU engine" />
|
||||
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
|
||||
<ComboBoxItem Content="Native" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Log level" />
|
||||
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
|
||||
<ComboBoxItem Content="Trace" />
|
||||
<ComboBoxItem Content="Debug" />
|
||||
<ComboBoxItem Content="Info" />
|
||||
<ComboBoxItem Content="Warning" />
|
||||
<ComboBoxItem Content="Error" />
|
||||
<ComboBoxItem Content="Critical" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
|
||||
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
||||
Value="0" FormatString="0" CornerRadius="8" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
||||
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Status bar -->
|
||||
<Grid Grid.Row="2" Background="{StaticResource ChromeBrush}" ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
|
||||
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock x:Name="StatusBarRight" Grid.Column="1" Text="" FontSize="11"
|
||||
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,36 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- Class library: the GUI is hosted by SharpEmu.CLI, which opens it when
|
||||
the executable is started without arguments. -->
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<Version>0.0.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Avalonia.Desktop" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" />
|
||||
<PackageReference Include="Tmds.DBus.Protocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="..\..\assets\images\logo.png" Link="Assets/logo.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- The DualSense HID reader is shared with the emulator's pad HLE. It is
|
||||
dependency-free, so it is compiled in directly rather than pulling a
|
||||
reference to all of SharpEmu.Libs into the launcher. -->
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,186 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Avalonia": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "11.3.18",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Native": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18",
|
||||
"Avalonia.Win32": "11.3.18",
|
||||
"Avalonia.X11": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.21.3, )",
|
||||
"resolved": "0.21.3",
|
||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||
},
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.BuildServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.2",
|
||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||
},
|
||||
"Avalonia.FreeDesktop": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Tmds.DBus.Protocol": "0.21.3"
|
||||
}
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Remote.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
||||
},
|
||||
"Avalonia.Skia": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"HarfBuzzSharp": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
||||
"SkiaSharp": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
||||
}
|
||||
},
|
||||
"Avalonia.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
||||
}
|
||||
},
|
||||
"Avalonia.X11": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.FreeDesktop": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
||||
"dependencies": {
|
||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"MicroCom.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.11.0",
|
||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||
},
|
||||
"SkiaSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||
"dependencies": {
|
||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,25 +2,20 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public sealed class CpuContext
|
||||
public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||
{
|
||||
private readonly ulong[] _registers = new ulong[16];
|
||||
private readonly ulong[] _xmmRegisters = new ulong[32];
|
||||
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
|
||||
private bool _raxWritten;
|
||||
|
||||
public CpuContext(ICpuMemory memory, Generation generation)
|
||||
{
|
||||
Memory = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||
TargetGeneration = generation;
|
||||
}
|
||||
public ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||
|
||||
public ICpuMemory Memory { get; }
|
||||
|
||||
public Generation TargetGeneration { get; }
|
||||
public Generation TargetGeneration { get; } = generation;
|
||||
|
||||
public ulong Rip { get; set; }
|
||||
|
||||
@@ -130,6 +125,91 @@ public sealed class CpuContext
|
||||
SetYmmUpper(registerIndex, highLow, highHigh);
|
||||
}
|
||||
|
||||
public bool TryReadByte(ulong address, out byte value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
if (!Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = buffer[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryReadUInt16(ulong address, out ushort value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||
if (!Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteUInt16(ulong address, ushort value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadInt32(ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteInt32(ulong address, int value, bool checkNil = false)
|
||||
{
|
||||
if (checkNil && address == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
public bool TryReadUInt32(ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteUInt32(ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryWriteInt64(ulong address, long value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(long)];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadUInt64(ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
@@ -150,6 +230,33 @@ public sealed class CpuContext
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadNullTerminatedUtf8(ulong address, int capacity, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (address == 0 || capacity <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytes[index] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool PushUInt64(ulong value)
|
||||
{
|
||||
var rsp = this[CpuRegister.Rsp];
|
||||
@@ -169,4 +276,23 @@ public sealed class CpuContext
|
||||
this[CpuRegister.Rsp] = rsp + sizeof(ulong);
|
||||
return true;
|
||||
}
|
||||
|
||||
public int SetReturn(int result, Type? cast = null)
|
||||
{
|
||||
var value = cast switch
|
||||
{
|
||||
null => (ulong)result,
|
||||
_ when cast == typeof(long) => (ulong)(long)result,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
this[CpuRegister.Rax] = unchecked(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
public int SetReturn(OrbisGen2Result result)
|
||||
{
|
||||
this[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -47,6 +47,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
var handler = CreateHandler(type, method, instances);
|
||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -105,6 +106,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return false;
|
||||
@@ -112,11 +114,13 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if ((export.Target & context.TargetGeneration) == 0)
|
||||
{
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
context.ClearRaxWriteFlag();
|
||||
int ret = ((SysAbiFunction)function).Invoke(context);
|
||||
|
||||
|
||||
@@ -8,6 +8,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||
</ItemGroup>
|
||||
|
||||
+4692
-175
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,313 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
internal enum Gen5ShaderEncoding
|
||||
{
|
||||
Sop1,
|
||||
Sop2,
|
||||
Sopc,
|
||||
Sopp,
|
||||
Sopk,
|
||||
Smrd,
|
||||
Smem,
|
||||
Mubuf,
|
||||
Mtbuf,
|
||||
Vop1,
|
||||
Vop2,
|
||||
Vopc,
|
||||
Vop3,
|
||||
Vintrp,
|
||||
Ds,
|
||||
Flat,
|
||||
Vop3p,
|
||||
Mimg,
|
||||
Exp,
|
||||
}
|
||||
|
||||
internal enum Gen5OperandKind
|
||||
{
|
||||
ScalarRegister,
|
||||
VectorRegister,
|
||||
EncodedConstant,
|
||||
LiteralConstant,
|
||||
}
|
||||
|
||||
internal enum Gen5ShaderResourceKind
|
||||
{
|
||||
ReadOnlyTexture,
|
||||
ReadWriteTexture,
|
||||
Sampler,
|
||||
ConstantBuffer,
|
||||
}
|
||||
|
||||
internal enum Gen5PixelOutputKind
|
||||
{
|
||||
Float,
|
||||
Uint,
|
||||
Sint,
|
||||
}
|
||||
|
||||
internal enum Gen5SpirvStage
|
||||
{
|
||||
Vertex,
|
||||
Pixel,
|
||||
Compute,
|
||||
}
|
||||
|
||||
internal sealed record Gen5SpirvShader(
|
||||
byte[] Spirv,
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
||||
uint AttributeCount,
|
||||
IReadOnlyList<Gen5VertexInputBinding> VertexInputs);
|
||||
|
||||
internal readonly record struct Gen5ShaderResourceMapping(
|
||||
Gen5ShaderResourceKind Kind,
|
||||
uint Slot,
|
||||
uint OffsetDwords,
|
||||
bool SizeFlag);
|
||||
|
||||
internal sealed record Gen5ShaderMetadata(
|
||||
uint ExtendedUserDataSizeDwords,
|
||||
uint ShaderResourceTableSizeDwords,
|
||||
IReadOnlyDictionary<uint, uint> DirectResources,
|
||||
IReadOnlyList<Gen5ShaderResourceMapping> Resources);
|
||||
|
||||
internal readonly record struct Gen5ComputeSystemRegisters(
|
||||
uint? WorkGroupXRegister,
|
||||
uint? WorkGroupYRegister,
|
||||
uint? WorkGroupZRegister,
|
||||
uint? ThreadGroupSizeRegister)
|
||||
{
|
||||
public bool TryGetExpression(uint scalarRegister, out string expression)
|
||||
{
|
||||
if (WorkGroupXRegister == scalarRegister)
|
||||
{
|
||||
expression = "gl_WorkGroupID.x";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (WorkGroupYRegister == scalarRegister)
|
||||
{
|
||||
expression = "gl_WorkGroupID.y";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (WorkGroupZRegister == scalarRegister)
|
||||
{
|
||||
expression = "gl_WorkGroupID.z";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ThreadGroupSizeRegister == scalarRegister)
|
||||
{
|
||||
expression = "(gl_WorkGroupSize.x * gl_WorkGroupSize.y * gl_WorkGroupSize.z)";
|
||||
return true;
|
||||
}
|
||||
|
||||
expression = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ClearStaticValues(Span<uint> scalarRegisters)
|
||||
{
|
||||
ClearStaticValue(scalarRegisters, WorkGroupXRegister);
|
||||
ClearStaticValue(scalarRegisters, WorkGroupYRegister);
|
||||
ClearStaticValue(scalarRegisters, WorkGroupZRegister);
|
||||
ClearStaticValue(scalarRegisters, ThreadGroupSizeRegister);
|
||||
}
|
||||
|
||||
private static void ClearStaticValue(Span<uint> scalarRegisters, uint? scalarRegister)
|
||||
{
|
||||
if (scalarRegister is { } register && register < scalarRegisters.Length)
|
||||
{
|
||||
scalarRegisters[(int)register] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record Gen5ShaderState(
|
||||
Gen5ShaderProgram Program,
|
||||
IReadOnlyList<uint> UserData,
|
||||
Gen5ShaderMetadata? Metadata,
|
||||
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
|
||||
uint UserDataScalarRegisterBase = 0);
|
||||
|
||||
internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
|
||||
{
|
||||
public static Gen5Operand Scalar(uint index) =>
|
||||
new(Gen5OperandKind.ScalarRegister, index);
|
||||
|
||||
public static Gen5Operand Vector(uint index) =>
|
||||
new(Gen5OperandKind.VectorRegister, index);
|
||||
|
||||
public static Gen5Operand Source(uint encoded, uint? literal = null)
|
||||
{
|
||||
if (encoded >= 256)
|
||||
{
|
||||
return Vector(encoded - 256);
|
||||
}
|
||||
|
||||
if (encoded is 249 or 255 && literal.HasValue)
|
||||
{
|
||||
return new(Gen5OperandKind.LiteralConstant, literal.Value);
|
||||
}
|
||||
|
||||
if (encoded <= 105 || encoded is 106 or 107 or 124 or 126 or 127)
|
||||
{
|
||||
return Scalar(encoded);
|
||||
}
|
||||
|
||||
return new(Gen5OperandKind.EncodedConstant, encoded);
|
||||
}
|
||||
|
||||
public override string ToString() => Kind switch
|
||||
{
|
||||
Gen5OperandKind.ScalarRegister => $"s{Value}",
|
||||
Gen5OperandKind.VectorRegister => $"v{Value}",
|
||||
Gen5OperandKind.LiteralConstant => $"0x{Value:X8}",
|
||||
_ => $"src[{Value}]",
|
||||
};
|
||||
}
|
||||
|
||||
internal abstract record Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5ImageControl(
|
||||
uint Dmask,
|
||||
uint VectorAddress,
|
||||
IReadOnlyList<uint> AddressRegisters,
|
||||
uint VectorData,
|
||||
uint ScalarResource,
|
||||
uint ScalarSampler,
|
||||
uint Dimension,
|
||||
bool IsArray,
|
||||
bool Glc,
|
||||
bool Slc) : Gen5InstructionControl
|
||||
{
|
||||
public uint GetAddressRegister(int component) =>
|
||||
component < AddressRegisters.Count
|
||||
? AddressRegisters[component]
|
||||
: VectorAddress + (uint)component;
|
||||
}
|
||||
|
||||
internal sealed record Gen5GlobalMemoryControl(
|
||||
uint DwordCount,
|
||||
uint VectorAddress,
|
||||
uint VectorData,
|
||||
uint ScalarAddress,
|
||||
int OffsetBytes,
|
||||
bool Glc,
|
||||
bool Slc) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5BufferMemoryControl(
|
||||
uint DwordCount,
|
||||
uint VectorAddress,
|
||||
uint VectorData,
|
||||
uint ScalarResource,
|
||||
int OffsetBytes,
|
||||
bool IndexEnabled,
|
||||
bool OffsetEnabled,
|
||||
bool Glc,
|
||||
bool Slc) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5ExportControl(
|
||||
uint Target,
|
||||
uint EnableMask,
|
||||
bool Compressed,
|
||||
bool Done,
|
||||
bool ValidMask) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5InterpolationControl(
|
||||
uint Attribute,
|
||||
uint Channel) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5Vop3Control(
|
||||
uint AbsoluteMask,
|
||||
uint NegateMask,
|
||||
uint OutputModifier,
|
||||
bool Clamp,
|
||||
uint? ScalarDestination) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5SdwaControl(
|
||||
uint DestinationSelect,
|
||||
uint Source0Select,
|
||||
uint Source1Select,
|
||||
uint AbsoluteMask,
|
||||
uint NegateMask,
|
||||
uint OutputModifier,
|
||||
bool Clamp) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5DppControl(
|
||||
uint Control,
|
||||
bool FetchInactive,
|
||||
bool BoundControl,
|
||||
uint AbsoluteMask,
|
||||
uint NegateMask,
|
||||
uint BankMask,
|
||||
uint RowMask) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5ScalarMemoryControl(
|
||||
uint DestinationCount,
|
||||
int ImmediateOffsetBytes,
|
||||
uint? DynamicOffsetRegister) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5DataShareControl(
|
||||
uint Offset0,
|
||||
uint Offset1,
|
||||
bool Gds) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5ImageBinding(
|
||||
uint Pc,
|
||||
string Opcode,
|
||||
Gen5ImageControl Control,
|
||||
IReadOnlyList<uint> ResourceDescriptor,
|
||||
IReadOnlyList<uint> SamplerDescriptor,
|
||||
uint? MipLevel);
|
||||
|
||||
internal sealed record Gen5GlobalMemoryBinding(
|
||||
uint ScalarAddress,
|
||||
ulong BaseAddress,
|
||||
IReadOnlyList<uint> InstructionPcs,
|
||||
byte[] Data);
|
||||
|
||||
internal sealed record Gen5VertexInputBinding(
|
||||
uint Pc,
|
||||
uint Location,
|
||||
uint ComponentCount,
|
||||
uint DataFormat,
|
||||
uint NumberFormat,
|
||||
ulong BaseAddress,
|
||||
uint Stride,
|
||||
uint OffsetBytes,
|
||||
byte[] Data);
|
||||
|
||||
internal sealed record Gen5ShaderEvaluation(
|
||||
IReadOnlyList<uint> InitialScalarRegisters,
|
||||
IReadOnlyList<uint> ScalarRegisters,
|
||||
IReadOnlyDictionary<uint, IReadOnlyList<uint>> ScalarRegistersByPc,
|
||||
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
|
||||
IReadOnlySet<uint>? RuntimeScalarRegisters = null,
|
||||
IReadOnlyList<Gen5VertexInputBinding>? VertexInputs = null);
|
||||
|
||||
internal sealed record Gen5ShaderInstruction(
|
||||
uint Pc,
|
||||
Gen5ShaderEncoding Encoding,
|
||||
string Opcode,
|
||||
IReadOnlyList<uint> Words,
|
||||
IReadOnlyList<Gen5Operand> Sources,
|
||||
IReadOnlyList<Gen5Operand> Destinations,
|
||||
Gen5InstructionControl? Control);
|
||||
|
||||
internal sealed record Gen5ShaderProgram(
|
||||
ulong Address,
|
||||
IReadOnlyList<Gen5ShaderInstruction> Instructions)
|
||||
{
|
||||
public IEnumerable<Gen5ImageControl> ImageResources =>
|
||||
Instructions
|
||||
.Select(instruction => instruction.Control)
|
||||
.OfType<Gen5ImageControl>();
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
internal static class Gen5ShaderMetadataReader
|
||||
{
|
||||
private const ulong ShaderUserDataOffset = 0x08;
|
||||
private const int ResourceClassCount = 4;
|
||||
private const int MaxMetadataEntries = 4096;
|
||||
|
||||
public static bool TryRead(
|
||||
CpuContext ctx,
|
||||
ulong shaderHeaderAddress,
|
||||
out Gen5ShaderMetadata metadata)
|
||||
{
|
||||
metadata = default!;
|
||||
if (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
||||
userDataAddress == 0 ||
|
||||
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var resourceOffsets = new ulong[ResourceClassCount];
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(
|
||||
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
|
||||
out resourceOffsets[resourceClass]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt16(userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||
!ctx.TryReadUInt16(userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||
!ctx.TryReadUInt16(userDataAddress + 0x2C, out var directResourceCount) ||
|
||||
directResourceCount > MaxMetadataEntries)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var resourceCounts = new ushort[ResourceClassCount];
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
if (!ctx.TryReadUInt16(
|
||||
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
|
||||
out resourceCounts[resourceClass]) ||
|
||||
resourceCounts[resourceClass] > MaxMetadataEntries)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var directResources = new Dictionary<uint, uint>();
|
||||
if (directResourceCount != 0)
|
||||
{
|
||||
if (directResourceOffsetsAddress == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint type = 0; type < directResourceCount; type++)
|
||||
{
|
||||
if (!ctx.TryReadUInt16(directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (offset != ushort.MaxValue)
|
||||
{
|
||||
directResources[type] = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var resources = new List<Gen5ShaderResourceMapping>();
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
var count = resourceCounts[resourceClass];
|
||||
if (count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resourceOffsets[resourceClass] == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint slot = 0; slot < count; slot++)
|
||||
{
|
||||
if (!ctx.TryReadUInt16(
|
||||
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
||||
out var sharp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = (uint)(sharp & 0x7FFF);
|
||||
if (offset == 0x7FFF)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
resources.Add(new Gen5ShaderResourceMapping(
|
||||
(Gen5ShaderResourceKind)resourceClass,
|
||||
slot,
|
||||
offset,
|
||||
(sharp & 0x8000) != 0));
|
||||
}
|
||||
}
|
||||
|
||||
metadata = new Gen5ShaderMetadata(
|
||||
extendedUserDataSize,
|
||||
shaderResourceTableSize,
|
||||
directResources,
|
||||
resources);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,167 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
internal static class SpirvFixedShaders
|
||||
{
|
||||
public static byte[] CreateFullscreenVertex(uint attributeCount)
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
module.AddCapability(SpirvCapability.Shader);
|
||||
|
||||
var voidType = module.TypeVoid();
|
||||
var boolType = module.TypeBool();
|
||||
var uintType = module.TypeInt(32, signed: false);
|
||||
var floatType = module.TypeFloat(32);
|
||||
var vec4Type = module.TypeVector(floatType, 4);
|
||||
var inputUintPointer = module.TypePointer(SpirvStorageClass.Input, uintType);
|
||||
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||
|
||||
var vertexIndex = module.AddGlobalVariable(inputUintPointer, SpirvStorageClass.Input);
|
||||
module.AddName(vertexIndex, "vertexIndex");
|
||||
module.AddDecoration(
|
||||
vertexIndex,
|
||||
SpirvDecoration.BuiltIn,
|
||||
(uint)SpirvBuiltIn.VertexIndex);
|
||||
|
||||
var position = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
|
||||
module.AddName(position, "position");
|
||||
module.AddDecoration(position, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Position);
|
||||
|
||||
var attributes = new uint[attributeCount];
|
||||
for (uint index = 0; index < attributeCount; index++)
|
||||
{
|
||||
attributes[index] =
|
||||
module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
|
||||
module.AddName(attributes[index], $"attr{index}");
|
||||
module.AddDecoration(attributes[index], SpirvDecoration.Location, index);
|
||||
module.AddDecoration(attributes[index], SpirvDecoration.NoPerspective);
|
||||
}
|
||||
|
||||
var functionType = module.TypeFunction(voidType);
|
||||
var main = module.BeginFunction(voidType, functionType);
|
||||
module.AddName(main, "main");
|
||||
module.AddLabel();
|
||||
|
||||
var indexValue = module.AddInstruction(SpirvOp.Load, uintType, vertexIndex);
|
||||
var one = module.Constant(uintType, 1);
|
||||
var two = module.Constant(uintType, 2);
|
||||
var shifted = module.AddInstruction(SpirvOp.ShiftLeftLogical, uintType, indexValue, one);
|
||||
var xBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, shifted, two);
|
||||
var yBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, indexValue, two);
|
||||
var x = module.AddInstruction(SpirvOp.ConvertUToF, floatType, xBits);
|
||||
var y = module.AddInstruction(SpirvOp.ConvertUToF, floatType, yBits);
|
||||
var zero = module.ConstantFloat(floatType, 0f);
|
||||
var oneFloat = module.ConstantFloat(floatType, 1f);
|
||||
var twoFloat = module.ConstantFloat(floatType, 2f);
|
||||
var xPosition = module.AddInstruction(SpirvOp.FMul, floatType, x, twoFloat);
|
||||
xPosition = module.AddInstruction(SpirvOp.FSub, floatType, xPosition, oneFloat);
|
||||
var yPosition = module.AddInstruction(SpirvOp.FMul, floatType, y, twoFloat);
|
||||
yPosition = module.AddInstruction(SpirvOp.FSub, floatType, yPosition, oneFloat);
|
||||
var positionValue = module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
vec4Type,
|
||||
xPosition,
|
||||
yPosition,
|
||||
zero,
|
||||
oneFloat);
|
||||
module.AddStatement(SpirvOp.Store, position, positionValue);
|
||||
|
||||
var attributeValue = module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
vec4Type,
|
||||
x,
|
||||
y,
|
||||
zero,
|
||||
oneFloat);
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
module.AddStatement(SpirvOp.Store, attribute, attributeValue);
|
||||
}
|
||||
|
||||
module.AddStatement(SpirvOp.Return);
|
||||
module.EndFunction();
|
||||
|
||||
var interfaces = new uint[2 + attributes.Length];
|
||||
interfaces[0] = vertexIndex;
|
||||
interfaces[1] = position;
|
||||
attributes.CopyTo(interfaces, 2);
|
||||
module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main", interfaces);
|
||||
_ = boolType;
|
||||
return module.Build();
|
||||
}
|
||||
|
||||
public static byte[] CreateCopyFragment()
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
module.AddCapability(SpirvCapability.Shader);
|
||||
|
||||
var voidType = module.TypeVoid();
|
||||
var floatType = module.TypeFloat(32);
|
||||
var vec2Type = module.TypeVector(floatType, 2);
|
||||
var vec4Type = module.TypeVector(floatType, 4);
|
||||
var inputVec4Pointer = module.TypePointer(SpirvStorageClass.Input, vec4Type);
|
||||
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||
var imageType = module.TypeImage(
|
||||
floatType,
|
||||
SpirvImageDim.Dim2D,
|
||||
depth: false,
|
||||
arrayed: false,
|
||||
multisampled: false,
|
||||
sampled: 1,
|
||||
SpirvImageFormat.Unknown);
|
||||
var sampledImageType = module.TypeSampledImage(imageType);
|
||||
var sampledImagePointer =
|
||||
module.TypePointer(SpirvStorageClass.UniformConstant, sampledImageType);
|
||||
|
||||
var attribute = module.AddGlobalVariable(inputVec4Pointer, SpirvStorageClass.Input);
|
||||
module.AddName(attribute, "attr0");
|
||||
module.AddDecoration(attribute, SpirvDecoration.Location, 0);
|
||||
|
||||
var texture = module.AddGlobalVariable(
|
||||
sampledImagePointer,
|
||||
SpirvStorageClass.UniformConstant);
|
||||
module.AddName(texture, "tex0");
|
||||
module.AddDecoration(texture, SpirvDecoration.DescriptorSet, 0);
|
||||
module.AddDecoration(texture, SpirvDecoration.Binding, 1);
|
||||
|
||||
var output = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
|
||||
module.AddName(output, "outColor");
|
||||
module.AddDecoration(output, SpirvDecoration.Location, 0);
|
||||
|
||||
var functionType = module.TypeFunction(voidType);
|
||||
var main = module.BeginFunction(voidType, functionType);
|
||||
module.AddName(main, "main");
|
||||
module.AddLabel();
|
||||
|
||||
var attributeValue = module.AddInstruction(SpirvOp.Load, vec4Type, attribute);
|
||||
var coordinates = module.AddInstruction(
|
||||
SpirvOp.VectorShuffle,
|
||||
vec2Type,
|
||||
attributeValue,
|
||||
attributeValue,
|
||||
0,
|
||||
1);
|
||||
var sampledImage = module.AddInstruction(SpirvOp.Load, sampledImageType, texture);
|
||||
var lod = module.ConstantFloat(floatType, 0f);
|
||||
var color = module.AddInstruction(
|
||||
SpirvOp.ImageSampleExplicitLod,
|
||||
vec4Type,
|
||||
sampledImage,
|
||||
coordinates,
|
||||
2,
|
||||
lod);
|
||||
module.AddStatement(SpirvOp.Store, output, color);
|
||||
module.AddStatement(SpirvOp.Return);
|
||||
module.EndFunction();
|
||||
|
||||
module.AddEntryPoint(
|
||||
SpirvExecutionModel.Fragment,
|
||||
main,
|
||||
"main",
|
||||
[attribute, texture, output]);
|
||||
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||
return module.Build();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,885 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
internal enum SpirvOp : ushort
|
||||
{
|
||||
Nop = 0,
|
||||
Name = 5,
|
||||
Extension = 10,
|
||||
ExtInstImport = 11,
|
||||
ExtInst = 12,
|
||||
MemoryModel = 14,
|
||||
EntryPoint = 15,
|
||||
ExecutionMode = 16,
|
||||
Capability = 17,
|
||||
TypeVoid = 19,
|
||||
TypeBool = 20,
|
||||
TypeInt = 21,
|
||||
TypeFloat = 22,
|
||||
TypeVector = 23,
|
||||
TypeImage = 25,
|
||||
TypeSampler = 26,
|
||||
TypeSampledImage = 27,
|
||||
TypeArray = 28,
|
||||
TypeRuntimeArray = 29,
|
||||
TypeStruct = 30,
|
||||
TypePointer = 32,
|
||||
TypeFunction = 33,
|
||||
ConstantTrue = 41,
|
||||
ConstantFalse = 42,
|
||||
Constant = 43,
|
||||
ConstantComposite = 44,
|
||||
ConstantNull = 46,
|
||||
Function = 54,
|
||||
FunctionParameter = 55,
|
||||
FunctionEnd = 56,
|
||||
FunctionCall = 57,
|
||||
Variable = 59,
|
||||
Load = 61,
|
||||
Store = 62,
|
||||
AccessChain = 65,
|
||||
ArrayLength = 68,
|
||||
Decorate = 71,
|
||||
VectorExtractDynamic = 77,
|
||||
VectorInsertDynamic = 78,
|
||||
VectorShuffle = 79,
|
||||
CompositeConstruct = 80,
|
||||
CompositeExtract = 81,
|
||||
CompositeInsert = 82,
|
||||
CopyObject = 83,
|
||||
SampledImage = 86,
|
||||
ImageSampleImplicitLod = 87,
|
||||
ImageSampleExplicitLod = 88,
|
||||
ImageSampleDrefImplicitLod = 89,
|
||||
ImageSampleDrefExplicitLod = 90,
|
||||
ImageFetch = 95,
|
||||
ImageGather = 96,
|
||||
ImageDrefGather = 97,
|
||||
ImageRead = 98,
|
||||
ImageWrite = 99,
|
||||
Image = 100,
|
||||
ImageQuerySizeLod = 103,
|
||||
ImageQuerySize = 104,
|
||||
ImageQueryLod = 105,
|
||||
ImageQueryLevels = 106,
|
||||
ImageQuerySamples = 107,
|
||||
ConvertFToU = 109,
|
||||
ConvertFToS = 110,
|
||||
ConvertSToF = 111,
|
||||
ConvertUToF = 112,
|
||||
UConvert = 113,
|
||||
SConvert = 114,
|
||||
FConvert = 115,
|
||||
Bitcast = 124,
|
||||
SNegate = 126,
|
||||
FNegate = 127,
|
||||
IAdd = 128,
|
||||
FAdd = 129,
|
||||
ISub = 130,
|
||||
FSub = 131,
|
||||
IMul = 132,
|
||||
FMul = 133,
|
||||
UDiv = 134,
|
||||
SDiv = 135,
|
||||
FDiv = 136,
|
||||
UMod = 137,
|
||||
SRem = 138,
|
||||
SMod = 139,
|
||||
FRem = 140,
|
||||
FMod = 141,
|
||||
IAddCarry = 149,
|
||||
ISubBorrow = 150,
|
||||
UMulExtended = 151,
|
||||
SMulExtended = 152,
|
||||
Any = 154,
|
||||
All = 155,
|
||||
IsNan = 156,
|
||||
IsInf = 157,
|
||||
LogicalEqual = 164,
|
||||
LogicalNotEqual = 165,
|
||||
LogicalOr = 166,
|
||||
LogicalAnd = 167,
|
||||
LogicalNot = 168,
|
||||
Select = 169,
|
||||
IEqual = 170,
|
||||
INotEqual = 171,
|
||||
UGreaterThan = 172,
|
||||
SGreaterThan = 173,
|
||||
UGreaterThanEqual = 174,
|
||||
SGreaterThanEqual = 175,
|
||||
ULessThan = 176,
|
||||
SLessThan = 177,
|
||||
ULessThanEqual = 178,
|
||||
SLessThanEqual = 179,
|
||||
FOrdEqual = 180,
|
||||
FUnordEqual = 181,
|
||||
FOrdNotEqual = 182,
|
||||
FUnordNotEqual = 183,
|
||||
FOrdLessThan = 184,
|
||||
FUnordLessThan = 185,
|
||||
FOrdGreaterThan = 186,
|
||||
FUnordGreaterThan = 187,
|
||||
FOrdLessThanEqual = 188,
|
||||
FUnordLessThanEqual = 189,
|
||||
FOrdGreaterThanEqual = 190,
|
||||
FUnordGreaterThanEqual = 191,
|
||||
ShiftRightLogical = 194,
|
||||
ShiftRightArithmetic = 195,
|
||||
ShiftLeftLogical = 196,
|
||||
BitwiseOr = 197,
|
||||
BitwiseXor = 198,
|
||||
BitwiseAnd = 199,
|
||||
Not = 200,
|
||||
BitFieldInsert = 201,
|
||||
BitFieldSExtract = 202,
|
||||
BitFieldUExtract = 203,
|
||||
BitReverse = 204,
|
||||
BitCount = 205,
|
||||
ControlBarrier = 224,
|
||||
MemoryBarrier = 225,
|
||||
AtomicIAdd = 234,
|
||||
Phi = 245,
|
||||
LoopMerge = 246,
|
||||
SelectionMerge = 247,
|
||||
Label = 248,
|
||||
Branch = 249,
|
||||
BranchConditional = 250,
|
||||
Switch = 251,
|
||||
Kill = 252,
|
||||
Return = 253,
|
||||
ReturnValue = 254,
|
||||
Unreachable = 255,
|
||||
GroupNonUniformElect = 333,
|
||||
GroupNonUniformAll = 334,
|
||||
GroupNonUniformAny = 335,
|
||||
GroupNonUniformAllEqual = 336,
|
||||
GroupNonUniformBroadcast = 337,
|
||||
GroupNonUniformBroadcastFirst = 338,
|
||||
GroupNonUniformBallot = 339,
|
||||
GroupNonUniformShuffle = 345,
|
||||
GroupNonUniformShuffleXor = 346,
|
||||
GroupNonUniformShuffleUp = 347,
|
||||
GroupNonUniformShuffleDown = 348,
|
||||
}
|
||||
|
||||
internal enum SpirvCapability : uint
|
||||
{
|
||||
Shader = 1,
|
||||
Float16 = 9,
|
||||
Float64 = 10,
|
||||
Int64 = 11,
|
||||
Int16 = 22,
|
||||
ImageGatherExtended = 25,
|
||||
StorageImageExtendedFormats = 49,
|
||||
ImageQuery = 50,
|
||||
StorageImageReadWithoutFormat = 55,
|
||||
StorageImageWriteWithoutFormat = 56,
|
||||
GroupNonUniform = 61,
|
||||
GroupNonUniformVote = 62,
|
||||
GroupNonUniformBallot = 64,
|
||||
GroupNonUniformShuffle = 65,
|
||||
RuntimeDescriptorArray = 5302,
|
||||
}
|
||||
|
||||
internal enum SpirvStorageClass : uint
|
||||
{
|
||||
UniformConstant = 0,
|
||||
Input = 1,
|
||||
Uniform = 2,
|
||||
Output = 3,
|
||||
Workgroup = 4,
|
||||
Private = 6,
|
||||
Function = 7,
|
||||
PushConstant = 9,
|
||||
Image = 11,
|
||||
StorageBuffer = 12,
|
||||
}
|
||||
|
||||
internal enum SpirvExecutionModel : uint
|
||||
{
|
||||
Vertex = 0,
|
||||
Fragment = 4,
|
||||
GLCompute = 5,
|
||||
}
|
||||
|
||||
internal enum SpirvExecutionMode : uint
|
||||
{
|
||||
OriginUpperLeft = 7,
|
||||
DepthReplacing = 12,
|
||||
LocalSize = 17,
|
||||
}
|
||||
|
||||
internal enum SpirvDecoration : uint
|
||||
{
|
||||
Block = 2,
|
||||
ArrayStride = 6,
|
||||
BuiltIn = 11,
|
||||
NoPerspective = 13,
|
||||
Flat = 14,
|
||||
Location = 30,
|
||||
Binding = 33,
|
||||
DescriptorSet = 34,
|
||||
Offset = 35,
|
||||
}
|
||||
|
||||
internal enum SpirvBuiltIn : uint
|
||||
{
|
||||
Position = 0,
|
||||
VertexIndex = 42,
|
||||
InstanceIndex = 43,
|
||||
FragCoord = 15,
|
||||
FrontFacing = 17,
|
||||
WorkgroupId = 26,
|
||||
LocalInvocationId = 27,
|
||||
GlobalInvocationId = 28,
|
||||
LocalInvocationIndex = 29,
|
||||
SubgroupLocalInvocationId = 41,
|
||||
}
|
||||
|
||||
internal enum SpirvImageDim : uint
|
||||
{
|
||||
Dim1D = 0,
|
||||
Dim2D = 1,
|
||||
Dim3D = 2,
|
||||
Cube = 3,
|
||||
Buffer = 5,
|
||||
}
|
||||
|
||||
internal enum SpirvImageFormat : uint
|
||||
{
|
||||
Unknown = 0,
|
||||
Rgba32f = 1,
|
||||
Rgba16f = 2,
|
||||
R32f = 3,
|
||||
Rgba8 = 4,
|
||||
Rgba8Snorm = 5,
|
||||
Rg32f = 6,
|
||||
Rg16f = 7,
|
||||
R11fG11fB10f = 8,
|
||||
R16f = 9,
|
||||
Rgba16 = 10,
|
||||
Rgb10A2 = 11,
|
||||
Rg16 = 12,
|
||||
Rg8 = 13,
|
||||
R16 = 14,
|
||||
R8 = 15,
|
||||
Rgba16Snorm = 16,
|
||||
Rg16Snorm = 17,
|
||||
Rg8Snorm = 18,
|
||||
R16Snorm = 19,
|
||||
R8Snorm = 20,
|
||||
Rgba32i = 21,
|
||||
Rgba16i = 22,
|
||||
Rgba8i = 23,
|
||||
R32i = 24,
|
||||
Rg32i = 25,
|
||||
Rg16i = 26,
|
||||
Rg8i = 27,
|
||||
R16i = 28,
|
||||
R8i = 29,
|
||||
Rgba32ui = 30,
|
||||
Rgba16ui = 31,
|
||||
Rgba8ui = 32,
|
||||
R32ui = 33,
|
||||
Rgb10A2ui = 34,
|
||||
Rg32ui = 35,
|
||||
Rg16ui = 36,
|
||||
Rg8ui = 37,
|
||||
R16ui = 38,
|
||||
R8ui = 39,
|
||||
}
|
||||
|
||||
internal sealed class SpirvModuleBuilder
|
||||
{
|
||||
private const uint Magic = 0x07230203;
|
||||
private const uint Version15 = 0x00010500;
|
||||
private const uint Generator = 0x53504500; // "SPE"
|
||||
|
||||
private readonly List<uint> _capabilities = [];
|
||||
private readonly List<uint> _extensions = [];
|
||||
private readonly List<uint> _imports = [];
|
||||
private readonly List<uint> _memoryModel = [];
|
||||
private readonly List<uint> _entryPoints = [];
|
||||
private readonly List<uint> _executionModes = [];
|
||||
private readonly List<uint> _debug = [];
|
||||
private readonly List<uint> _annotations = [];
|
||||
private readonly List<uint> _typesConstantsGlobals = [];
|
||||
private readonly List<uint> _functions = [];
|
||||
private readonly Dictionary<(uint Width, bool Signed), uint> _integerTypes = [];
|
||||
private readonly Dictionary<uint, uint> _floatTypes = [];
|
||||
private readonly Dictionary<(uint Component, uint Count), uint> _vectorTypes = [];
|
||||
private readonly Dictionary<
|
||||
(
|
||||
uint SampledType,
|
||||
SpirvImageDim Dimension,
|
||||
bool Depth,
|
||||
bool Arrayed,
|
||||
bool Multisampled,
|
||||
uint Sampled,
|
||||
SpirvImageFormat Format
|
||||
),
|
||||
uint> _imageTypes = [];
|
||||
private readonly Dictionary<uint, uint> _sampledImageTypes = [];
|
||||
private readonly Dictionary<(SpirvStorageClass Storage, uint Type), uint> _pointerTypes = [];
|
||||
private readonly Dictionary<(uint Element, uint Count), uint> _arrayTypes = [];
|
||||
private readonly Dictionary<uint, uint> _runtimeArrayTypes = [];
|
||||
private readonly Dictionary<string, uint> _functionTypes = [];
|
||||
private readonly Dictionary<(uint Type, ulong Value), uint> _constants = [];
|
||||
private readonly HashSet<SpirvCapability> _declaredCapabilities = [];
|
||||
private readonly Dictionary<string, uint> _extInstImports = [];
|
||||
private uint _nextId = 1;
|
||||
private uint? _voidType;
|
||||
private uint? _boolType;
|
||||
|
||||
public uint AllocateId() => _nextId++;
|
||||
|
||||
public void AddCapability(SpirvCapability capability)
|
||||
{
|
||||
if (_declaredCapabilities.Add(capability))
|
||||
{
|
||||
Emit(_capabilities, SpirvOp.Capability, (uint)capability);
|
||||
}
|
||||
}
|
||||
|
||||
public void AddExtension(string extension) =>
|
||||
EmitWithString(_extensions, SpirvOp.Extension, [], extension);
|
||||
|
||||
public uint ImportExtInst(string name)
|
||||
{
|
||||
if (_extInstImports.TryGetValue(name, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
EmitWithString(_imports, SpirvOp.ExtInstImport, [id], name);
|
||||
_extInstImports.Add(name, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public void SetLogicalGlsl450MemoryModel() =>
|
||||
Emit(_memoryModel, SpirvOp.MemoryModel, 0, 1);
|
||||
|
||||
public void AddEntryPoint(
|
||||
SpirvExecutionModel model,
|
||||
uint function,
|
||||
string name,
|
||||
IReadOnlyList<uint> interfaces)
|
||||
{
|
||||
var prefix = new uint[2 + interfaces.Count];
|
||||
prefix[0] = (uint)model;
|
||||
prefix[1] = function;
|
||||
for (var index = 0; index < interfaces.Count; index++)
|
||||
{
|
||||
prefix[index + 2] = interfaces[index];
|
||||
}
|
||||
|
||||
EmitWithString(_entryPoints, SpirvOp.EntryPoint, prefix, name, stringBeforeTailCount: 2);
|
||||
}
|
||||
|
||||
public void AddExecutionMode(uint function, SpirvExecutionMode mode, params uint[] operands)
|
||||
{
|
||||
var values = new uint[2 + operands.Length];
|
||||
values[0] = function;
|
||||
values[1] = (uint)mode;
|
||||
operands.CopyTo(values, 2);
|
||||
Emit(_executionModes, SpirvOp.ExecutionMode, values);
|
||||
}
|
||||
|
||||
public void AddName(uint target, string name) =>
|
||||
EmitWithString(_debug, SpirvOp.Name, [target], name);
|
||||
|
||||
public void AddDecoration(uint target, SpirvDecoration decoration, params uint[] operands)
|
||||
{
|
||||
var values = new uint[2 + operands.Length];
|
||||
values[0] = target;
|
||||
values[1] = (uint)decoration;
|
||||
operands.CopyTo(values, 2);
|
||||
Emit(_annotations, SpirvOp.Decorate, values);
|
||||
}
|
||||
|
||||
public void AddMemberDecoration(
|
||||
uint target,
|
||||
uint member,
|
||||
SpirvDecoration decoration,
|
||||
params uint[] operands)
|
||||
{
|
||||
var values = new uint[3 + operands.Length];
|
||||
values[0] = target;
|
||||
values[1] = member;
|
||||
values[2] = (uint)decoration;
|
||||
operands.CopyTo(values, 3);
|
||||
EmitRaw(_annotations, 72, values);
|
||||
}
|
||||
|
||||
public uint TypeVoid()
|
||||
{
|
||||
if (_voidType is { } existing)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(_typesConstantsGlobals, SpirvOp.TypeVoid, id);
|
||||
_voidType = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint TypeBool()
|
||||
{
|
||||
if (_boolType is { } existing)
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(_typesConstantsGlobals, SpirvOp.TypeBool, id);
|
||||
_boolType = id;
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint TypeInt(uint width, bool signed)
|
||||
{
|
||||
var key = (width, signed);
|
||||
if (_integerTypes.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(_typesConstantsGlobals, SpirvOp.TypeInt, id, width, signed ? 1u : 0u);
|
||||
_integerTypes.Add(key, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint TypeFloat(uint width)
|
||||
{
|
||||
if (_floatTypes.TryGetValue(width, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(_typesConstantsGlobals, SpirvOp.TypeFloat, id, width);
|
||||
_floatTypes.Add(width, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint TypeVector(uint componentType, uint count)
|
||||
{
|
||||
var key = (componentType, count);
|
||||
if (_vectorTypes.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(_typesConstantsGlobals, SpirvOp.TypeVector, id, componentType, count);
|
||||
_vectorTypes.Add(key, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint TypeImage(
|
||||
uint sampledType,
|
||||
SpirvImageDim dimension,
|
||||
bool depth,
|
||||
bool arrayed,
|
||||
bool multisampled,
|
||||
uint sampled,
|
||||
SpirvImageFormat format)
|
||||
{
|
||||
var key = (
|
||||
sampledType,
|
||||
dimension,
|
||||
depth,
|
||||
arrayed,
|
||||
multisampled,
|
||||
sampled,
|
||||
format);
|
||||
if (_imageTypes.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(
|
||||
_typesConstantsGlobals,
|
||||
SpirvOp.TypeImage,
|
||||
id,
|
||||
sampledType,
|
||||
(uint)dimension,
|
||||
depth ? 1u : 0u,
|
||||
arrayed ? 1u : 0u,
|
||||
multisampled ? 1u : 0u,
|
||||
sampled,
|
||||
(uint)format);
|
||||
_imageTypes.Add(key, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint TypeSampledImage(uint imageType)
|
||||
{
|
||||
if (_sampledImageTypes.TryGetValue(imageType, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(_typesConstantsGlobals, SpirvOp.TypeSampledImage, id, imageType);
|
||||
_sampledImageTypes.Add(imageType, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint TypeArray(uint elementType, uint count)
|
||||
{
|
||||
var key = (elementType, count);
|
||||
if (_arrayTypes.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var length = Constant(TypeInt(32, false), count);
|
||||
var id = AllocateId();
|
||||
Emit(_typesConstantsGlobals, SpirvOp.TypeArray, id, elementType, length);
|
||||
_arrayTypes.Add(key, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint TypeRuntimeArray(uint elementType)
|
||||
{
|
||||
if (_runtimeArrayTypes.TryGetValue(elementType, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(_typesConstantsGlobals, SpirvOp.TypeRuntimeArray, id, elementType);
|
||||
_runtimeArrayTypes.Add(elementType, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint TypeStruct(params uint[] memberTypes)
|
||||
{
|
||||
var id = AllocateId();
|
||||
var operands = new uint[memberTypes.Length + 1];
|
||||
operands[0] = id;
|
||||
memberTypes.CopyTo(operands, 1);
|
||||
Emit(_typesConstantsGlobals, SpirvOp.TypeStruct, operands);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint TypePointer(SpirvStorageClass storageClass, uint type)
|
||||
{
|
||||
var key = (storageClass, type);
|
||||
if (_pointerTypes.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(_typesConstantsGlobals, SpirvOp.TypePointer, id, (uint)storageClass, type);
|
||||
_pointerTypes.Add(key, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint TypeFunction(uint returnType, params uint[] parameterTypes)
|
||||
{
|
||||
var key = returnType + ":" + string.Join(',', parameterTypes);
|
||||
if (_functionTypes.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
var operands = new uint[parameterTypes.Length + 2];
|
||||
operands[0] = id;
|
||||
operands[1] = returnType;
|
||||
parameterTypes.CopyTo(operands, 2);
|
||||
Emit(_typesConstantsGlobals, SpirvOp.TypeFunction, operands);
|
||||
_functionTypes.Add(key, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint ConstantBool(bool value)
|
||||
{
|
||||
var type = TypeBool();
|
||||
var key = (type, value ? 1UL : 0UL);
|
||||
if (_constants.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(
|
||||
_typesConstantsGlobals,
|
||||
value ? SpirvOp.ConstantTrue : SpirvOp.ConstantFalse,
|
||||
type,
|
||||
id);
|
||||
_constants.Add(key, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint Constant(uint type, uint value)
|
||||
{
|
||||
var key = (type, (ulong)value);
|
||||
if (_constants.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(_typesConstantsGlobals, SpirvOp.Constant, type, id, value);
|
||||
_constants.Add(key, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint Constant64(uint type, ulong value)
|
||||
{
|
||||
var key = (type, value);
|
||||
if (_constants.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(
|
||||
_typesConstantsGlobals,
|
||||
SpirvOp.Constant,
|
||||
type,
|
||||
id,
|
||||
(uint)value,
|
||||
(uint)(value >> 32));
|
||||
_constants.Add(key, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint ConstantFloat(uint type, float value) =>
|
||||
Constant(type, BitConverter.SingleToUInt32Bits(value));
|
||||
|
||||
public uint ConstantComposite(uint type, params uint[] constituents)
|
||||
{
|
||||
var id = AllocateId();
|
||||
var operands = new uint[constituents.Length + 2];
|
||||
operands[0] = type;
|
||||
operands[1] = id;
|
||||
constituents.CopyTo(operands, 2);
|
||||
Emit(_typesConstantsGlobals, SpirvOp.ConstantComposite, operands);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint ConstantNull(uint type)
|
||||
{
|
||||
var key = (type, ulong.MaxValue);
|
||||
if (_constants.TryGetValue(key, out var existing))
|
||||
{
|
||||
return existing;
|
||||
}
|
||||
|
||||
var id = AllocateId();
|
||||
Emit(_typesConstantsGlobals, SpirvOp.ConstantNull, type, id);
|
||||
_constants.Add(key, id);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint AddGlobalVariable(
|
||||
uint pointerType,
|
||||
SpirvStorageClass storageClass,
|
||||
uint? initializer = null)
|
||||
{
|
||||
var id = AllocateId();
|
||||
if (initializer.HasValue)
|
||||
{
|
||||
Emit(
|
||||
_typesConstantsGlobals,
|
||||
SpirvOp.Variable,
|
||||
pointerType,
|
||||
id,
|
||||
(uint)storageClass,
|
||||
initializer.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Emit(_typesConstantsGlobals, SpirvOp.Variable, pointerType, id, (uint)storageClass);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint BeginFunction(uint returnType, uint functionType)
|
||||
{
|
||||
var id = AllocateId();
|
||||
Emit(_functions, SpirvOp.Function, returnType, id, 0, functionType);
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint AddFunctionParameter(uint type) =>
|
||||
EmitResult(_functions, SpirvOp.FunctionParameter, type);
|
||||
|
||||
public uint AddLabel(uint? id = null)
|
||||
{
|
||||
var result = id ?? AllocateId();
|
||||
Emit(_functions, SpirvOp.Label, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
public uint AddFunctionVariable(uint pointerType, uint? initializer = null)
|
||||
{
|
||||
var id = AllocateId();
|
||||
if (initializer.HasValue)
|
||||
{
|
||||
Emit(
|
||||
_functions,
|
||||
SpirvOp.Variable,
|
||||
pointerType,
|
||||
id,
|
||||
(uint)SpirvStorageClass.Function,
|
||||
initializer.Value);
|
||||
}
|
||||
else
|
||||
{
|
||||
Emit(
|
||||
_functions,
|
||||
SpirvOp.Variable,
|
||||
pointerType,
|
||||
id,
|
||||
(uint)SpirvStorageClass.Function);
|
||||
}
|
||||
|
||||
return id;
|
||||
}
|
||||
|
||||
public uint AddInstruction(SpirvOp opcode, uint resultType, params uint[] operands) =>
|
||||
EmitResult(_functions, opcode, resultType, operands);
|
||||
|
||||
public void AddStatement(SpirvOp opcode, params uint[] operands) =>
|
||||
Emit(_functions, opcode, operands);
|
||||
|
||||
public void EndFunction() => Emit(_functions, SpirvOp.FunctionEnd);
|
||||
|
||||
public byte[] Build()
|
||||
{
|
||||
if (_memoryModel.Count == 0)
|
||||
{
|
||||
SetLogicalGlsl450MemoryModel();
|
||||
}
|
||||
|
||||
var wordCount =
|
||||
5 +
|
||||
_capabilities.Count +
|
||||
_extensions.Count +
|
||||
_imports.Count +
|
||||
_memoryModel.Count +
|
||||
_entryPoints.Count +
|
||||
_executionModes.Count +
|
||||
_debug.Count +
|
||||
_annotations.Count +
|
||||
_typesConstantsGlobals.Count +
|
||||
_functions.Count;
|
||||
var words = new uint[wordCount];
|
||||
var offset = 0;
|
||||
WriteWord(Magic);
|
||||
WriteWord(Version15);
|
||||
WriteWord(Generator);
|
||||
WriteWord(_nextId);
|
||||
WriteWord(0);
|
||||
WriteSection(_capabilities);
|
||||
WriteSection(_extensions);
|
||||
WriteSection(_imports);
|
||||
WriteSection(_memoryModel);
|
||||
WriteSection(_entryPoints);
|
||||
WriteSection(_executionModes);
|
||||
WriteSection(_debug);
|
||||
WriteSection(_annotations);
|
||||
WriteSection(_typesConstantsGlobals);
|
||||
WriteSection(_functions);
|
||||
var bytes = new byte[wordCount * sizeof(uint)];
|
||||
Buffer.BlockCopy(words, 0, bytes, 0, bytes.Length);
|
||||
return bytes;
|
||||
|
||||
void WriteWord(uint value)
|
||||
{
|
||||
words[offset++] = value;
|
||||
}
|
||||
|
||||
void WriteSection(List<uint> section)
|
||||
{
|
||||
foreach (var value in section)
|
||||
{
|
||||
WriteWord(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private uint EmitResult(
|
||||
List<uint> section,
|
||||
SpirvOp opcode,
|
||||
uint resultType,
|
||||
params uint[] operands)
|
||||
{
|
||||
var result = AllocateId();
|
||||
var values = new uint[operands.Length + 2];
|
||||
values[0] = resultType;
|
||||
values[1] = result;
|
||||
operands.CopyTo(values, 2);
|
||||
Emit(section, opcode, values);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void Emit(List<uint> section, SpirvOp opcode, params uint[] operands) =>
|
||||
EmitRaw(section, (ushort)opcode, operands);
|
||||
|
||||
private static void EmitRaw(List<uint> section, ushort opcode, params uint[] operands)
|
||||
{
|
||||
section.Add(((uint)(operands.Length + 1) << 16) | opcode);
|
||||
section.AddRange(operands);
|
||||
}
|
||||
|
||||
private static void EmitWithString(
|
||||
List<uint> section,
|
||||
SpirvOp opcode,
|
||||
IReadOnlyList<uint> prefix,
|
||||
string value,
|
||||
int stringBeforeTailCount = -1)
|
||||
{
|
||||
var encoded = EncodeString(value);
|
||||
if (stringBeforeTailCount < 0)
|
||||
{
|
||||
var operands = new uint[prefix.Count + encoded.Length];
|
||||
for (var index = 0; index < prefix.Count; index++)
|
||||
{
|
||||
operands[index] = prefix[index];
|
||||
}
|
||||
|
||||
encoded.CopyTo(operands, prefix.Count);
|
||||
Emit(section, opcode, operands);
|
||||
return;
|
||||
}
|
||||
|
||||
var result = new uint[prefix.Count + encoded.Length];
|
||||
for (var index = 0; index < stringBeforeTailCount; index++)
|
||||
{
|
||||
result[index] = prefix[index];
|
||||
}
|
||||
|
||||
encoded.CopyTo(result, stringBeforeTailCount);
|
||||
for (var index = stringBeforeTailCount; index < prefix.Count; index++)
|
||||
{
|
||||
result[index + encoded.Length] = prefix[index];
|
||||
}
|
||||
|
||||
Emit(section, opcode, result);
|
||||
}
|
||||
|
||||
private static uint[] EncodeString(string value)
|
||||
{
|
||||
var byteCount = Encoding.UTF8.GetByteCount(value) + 1;
|
||||
var words = new uint[(byteCount + 3) / 4];
|
||||
Encoding.UTF8.GetBytes(value, System.Runtime.InteropServices.MemoryMarshal.AsBytes(words.AsSpan()));
|
||||
return words;
|
||||
}
|
||||
}
|
||||
@@ -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,17 @@ 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 ConcurrentDictionary<string, Lazy<CachedHostFile>> _hostFileCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
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
|
||||
{
|
||||
@@ -28,6 +38,23 @@ public static class AmprExports
|
||||
public ulong WriteOffset;
|
||||
}
|
||||
|
||||
private sealed class CachedHostFile
|
||||
{
|
||||
public CachedHostFile(string path)
|
||||
{
|
||||
Stream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite | FileShare.Delete,
|
||||
bufferSize: 1024 * 1024,
|
||||
FileOptions.RandomAccess);
|
||||
}
|
||||
|
||||
public object Gate { get; } = new();
|
||||
public FileStream Stream { get; }
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8aI7R7WaOlc",
|
||||
ExportName = "sceAmprCommandBufferConstructor",
|
||||
@@ -72,12 +99,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 +123,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 +199,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;
|
||||
@@ -237,7 +267,7 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
|
||||
{
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
@@ -284,6 +314,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 +382,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 (!ctx.TryReadUInt32(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 +501,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 +543,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 +570,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 +590,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,23 +647,48 @@ 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);
|
||||
if (fileOffset >= (ulong)stream.Length)
|
||||
if (!TryGetCachedHostFile(hostPath, out var cachedFile, out var openResult))
|
||||
{
|
||||
return openResult;
|
||||
}
|
||||
|
||||
long fileLength;
|
||||
lock (cachedFile.Gate)
|
||||
{
|
||||
fileLength = cachedFile.Stream.Length;
|
||||
}
|
||||
|
||||
if (fileOffset >= (ulong)fileLength)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
stream.Position = unchecked((long)fileOffset);
|
||||
|
||||
while (bytesRead < size)
|
||||
{
|
||||
if (bytesRead > ulong.MaxValue - fileOffset)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var absoluteOffset = fileOffset + bytesRead;
|
||||
if (absoluteOffset > long.MaxValue)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
||||
var read = stream.Read(buffer, 0, request);
|
||||
int read;
|
||||
lock (cachedFile.Gate)
|
||||
{
|
||||
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
|
||||
read = cachedFile.Stream.Read(buffer, 0, request);
|
||||
}
|
||||
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
@@ -528,10 +710,52 @@ public static class AmprExports
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryGetCachedHostFile(string hostPath, out CachedHostFile file, out int result)
|
||||
{
|
||||
file = null!;
|
||||
result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
|
||||
string cachePath;
|
||||
try
|
||||
{
|
||||
cachePath = Path.GetFullPath(hostPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
cachePath = hostPath;
|
||||
}
|
||||
|
||||
var lazy = _hostFileCache.GetOrAdd(
|
||||
cachePath,
|
||||
static path => new Lazy<CachedHostFile>(() => new CachedHostFile(path), isThreadSafe: true));
|
||||
|
||||
try
|
||||
{
|
||||
file = lazy.Value;
|
||||
return true;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
_hostFileCache.TryRemove(cachePath, out _);
|
||||
result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
return false;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
_hostFileCache.TryRemove(cachePath, out _);
|
||||
result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AppendReadFileRecord(
|
||||
CpuContext ctx,
|
||||
ulong commandBuffer,
|
||||
@@ -558,20 +782,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 +836,56 @@ 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 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 +907,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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
int value;
|
||||
if (paramId == AppParamSkuFlag)
|
||||
{
|
||||
value = AppParamSkuFlagFull;
|
||||
}
|
||||
else if (!TryReadUserDefinedParam(paramId, out value))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||
if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceAppContent($"app_param_get_int id={paramId} value={value}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "buYbeLOGWmA",
|
||||
ExportName = "sceAppContentTemporaryDataMount2",
|
||||
@@ -83,6 +121,63 @@ 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 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,217 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
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 ctx.SetReturn((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)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((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 ctx.SetReturn((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)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "on6ZH7Abo10",
|
||||
ExportName = "sceAudioOut2ContextDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(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 ctx.SetReturn((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 ctx.TryWriteUInt64(outPortAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((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 ctx.SetReturn((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)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((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 ctx.SetReturn((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)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((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) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "IaZXJ9M79uo",
|
||||
ExportName = "sceAudioOut2UserDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||
return ctx.TryWriteUInt64(outUserAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,234 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class AudioOutExports
|
||||
{
|
||||
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
||||
private static int _nextPortHandle;
|
||||
|
||||
private sealed class PortState : IDisposable
|
||||
{
|
||||
private readonly object _paceGate = new();
|
||||
private long _nextSilentOutput;
|
||||
|
||||
public PortState(
|
||||
int userId,
|
||||
int type,
|
||||
uint bufferLength,
|
||||
uint frequency,
|
||||
int format,
|
||||
int channels,
|
||||
int bytesPerSample,
|
||||
bool isFloat,
|
||||
WinMmAudioPort? backend)
|
||||
{
|
||||
UserId = userId;
|
||||
Type = type;
|
||||
BufferLength = bufferLength;
|
||||
Frequency = frequency;
|
||||
Format = format;
|
||||
Channels = channels;
|
||||
BytesPerSample = bytesPerSample;
|
||||
IsFloat = isFloat;
|
||||
Backend = backend;
|
||||
}
|
||||
|
||||
public int UserId { get; }
|
||||
public int Type { get; }
|
||||
public uint BufferLength { get; }
|
||||
public uint Frequency { get; }
|
||||
public int Format { get; }
|
||||
public int Channels { get; }
|
||||
public int BytesPerSample { get; }
|
||||
public bool IsFloat { get; }
|
||||
public WinMmAudioPort? Backend { get; }
|
||||
public int BufferByteLength =>
|
||||
checked((int)BufferLength * Channels * BytesPerSample);
|
||||
|
||||
public void PaceSilence()
|
||||
{
|
||||
long delay;
|
||||
lock (_paceGate)
|
||||
{
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
if (_nextSilentOutput < now)
|
||||
{
|
||||
_nextSilentOutput = now;
|
||||
}
|
||||
|
||||
delay = _nextSilentOutput - now;
|
||||
_nextSilentOutput += checked(
|
||||
(long)Math.Ceiling(
|
||||
Stopwatch.Frequency * (double)BufferLength / Frequency));
|
||||
}
|
||||
|
||||
if (delay > 0)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose() => Backend?.Dispose();
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JfEPXVxhFqA",
|
||||
ExportName = "sceAudioOutInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut")]
|
||||
public static int AudioOutInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ekNvsT22rsY",
|
||||
ExportName = "sceAudioOutOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut")]
|
||||
public static int AudioOutOpen(CpuContext ctx)
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var bufferLength = unchecked((uint)ctx[CpuRegister.Rcx]);
|
||||
var frequency = unchecked((uint)ctx[CpuRegister.R8]);
|
||||
var format = unchecked((int)ctx[CpuRegister.R9]);
|
||||
if (bufferLength == 0 || frequency == 0 ||
|
||||
!TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
WinMmAudioPort? backend = null;
|
||||
string backendName;
|
||||
try
|
||||
{
|
||||
backend = new WinMmAudioPort(frequency);
|
||||
backendName = "winmm";
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
backendName = "silent";
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] AudioOut host backend unavailable: {exception.Message}");
|
||||
}
|
||||
|
||||
var handle = Interlocked.Increment(ref _nextPortHandle);
|
||||
Ports[handle] = new PortState(
|
||||
userId,
|
||||
type,
|
||||
bufferLength,
|
||||
frequency,
|
||||
format,
|
||||
channels,
|
||||
bytesPerSample,
|
||||
isFloat,
|
||||
backend);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
|
||||
$"{channels} ch, {(isFloat ? "float32" : "s16")}, " +
|
||||
$"{bufferLength} frames, backend={backendName}");
|
||||
return ctx.SetReturn(handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "s1--uE9mBFw",
|
||||
ExportName = "sceAudioOutClose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut")]
|
||||
public static int AudioOutClose(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!Ports.TryRemove(handle, out var port))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
port.Dispose();
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QOQtbeDqsT4",
|
||||
ExportName = "sceAudioOutOutput",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut")]
|
||||
public static int AudioOutOutput(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||
if (!Ports.TryGetValue(handle, out var port))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (sourceAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
|
||||
try
|
||||
{
|
||||
var source = buffer.AsSpan(0, port.BufferByteLength);
|
||||
if (!ctx.Memory.TryRead(sourceAddress, source))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (port.Backend is null ||
|
||||
!port.Backend.Submit(
|
||||
source,
|
||||
port.BufferLength,
|
||||
port.Channels,
|
||||
port.BytesPerSample,
|
||||
port.IsFloat))
|
||||
{
|
||||
port.PaceSilence();
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "b+uAV89IlxE",
|
||||
ExportName = "sceAudioOutSetVolume",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut")]
|
||||
public static int AudioOutSetVolume(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return ctx.SetReturn(
|
||||
Ports.ContainsKey(handle)
|
||||
? 0
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
private static bool TryGetFormat(
|
||||
int rawFormat,
|
||||
out int channels,
|
||||
out int bytesPerSample,
|
||||
out bool isFloat)
|
||||
{
|
||||
var format = rawFormat & 0xFF;
|
||||
channels = format switch
|
||||
{
|
||||
0 or 3 => 1,
|
||||
1 or 4 => 2,
|
||||
2 or 5 or 6 or 7 => 8,
|
||||
_ => 0,
|
||||
};
|
||||
bytesPerSample = format is >= 3 and <= 5 or 7 ? 4 : 2;
|
||||
isFloat = bytesPerSample == 4;
|
||||
return channels != 0;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,302 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
internal sealed class WinMmAudioPort : IDisposable
|
||||
{
|
||||
private const uint WaveMapper = uint.MaxValue;
|
||||
private const uint CallbackEvent = 0x0005_0000;
|
||||
private const ushort WaveFormatPcm = 1;
|
||||
private const uint WaveHeaderDone = 0x0000_0001;
|
||||
private const int MaximumQueuedPcmBytes = 32 * 1024;
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly AutoResetEvent _completion = new(false);
|
||||
private readonly Queue<NativeBuffer> _buffers = new();
|
||||
private IntPtr _device;
|
||||
private int _queuedPcmBytes;
|
||||
private bool _disposed;
|
||||
|
||||
public WinMmAudioPort(uint sampleRate)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
|
||||
}
|
||||
|
||||
var format = new WaveFormat
|
||||
{
|
||||
FormatTag = WaveFormatPcm,
|
||||
Channels = 2,
|
||||
SamplesPerSecond = sampleRate,
|
||||
AverageBytesPerSecond = checked(sampleRate * 4),
|
||||
BlockAlign = 4,
|
||||
BitsPerSample = 16,
|
||||
ExtraSize = 0,
|
||||
};
|
||||
var result = WaveOutOpen(
|
||||
out _device,
|
||||
WaveMapper,
|
||||
ref format,
|
||||
_completion.SafeWaitHandle.DangerousGetHandle(),
|
||||
IntPtr.Zero,
|
||||
CallbackEvent);
|
||||
if (result != 0)
|
||||
{
|
||||
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
|
||||
}
|
||||
}
|
||||
|
||||
public bool Submit(
|
||||
ReadOnlySpan<byte> source,
|
||||
uint frames,
|
||||
int channels,
|
||||
int bytesPerSample,
|
||||
bool isFloat)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var outputLength = checked((int)frames * 4);
|
||||
ReapCompletedBuffers();
|
||||
while (_queuedPcmBytes != 0 &&
|
||||
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
|
||||
{
|
||||
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
ReapCompletedBuffers();
|
||||
}
|
||||
|
||||
var output = ArrayPool<byte>.Shared.Rent(outputLength);
|
||||
try
|
||||
{
|
||||
ConvertToStereoPcm16(
|
||||
source,
|
||||
output.AsSpan(0, outputLength),
|
||||
checked((int)frames),
|
||||
channels,
|
||||
bytesPerSample,
|
||||
isFloat);
|
||||
return QueueBuffer(output.AsSpan(0, outputLength));
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(output);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
if (_device != IntPtr.Zero)
|
||||
{
|
||||
WaveOutReset(_device);
|
||||
while (_buffers.TryDequeue(out var buffer))
|
||||
{
|
||||
ReleaseBuffer(buffer);
|
||||
}
|
||||
|
||||
WaveOutClose(_device);
|
||||
_device = IntPtr.Zero;
|
||||
}
|
||||
|
||||
_completion.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private bool QueueBuffer(ReadOnlySpan<byte> data)
|
||||
{
|
||||
var dataAddress = Marshal.AllocHGlobal(data.Length);
|
||||
var headerAddress = IntPtr.Zero;
|
||||
try
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
|
||||
}
|
||||
|
||||
var header = new WaveHeader
|
||||
{
|
||||
Data = dataAddress,
|
||||
BufferLength = checked((uint)data.Length),
|
||||
};
|
||||
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
|
||||
Marshal.StructureToPtr(header, headerAddress, false);
|
||||
|
||||
var result = WaveOutPrepareHeader(
|
||||
_device,
|
||||
headerAddress,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
if (result != 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
result = WaveOutWrite(
|
||||
_device,
|
||||
headerAddress,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
if (result != 0)
|
||||
{
|
||||
WaveOutUnprepareHeader(
|
||||
_device,
|
||||
headerAddress,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
return false;
|
||||
}
|
||||
|
||||
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
|
||||
_queuedPcmBytes += data.Length;
|
||||
dataAddress = IntPtr.Zero;
|
||||
headerAddress = IntPtr.Zero;
|
||||
return true;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (headerAddress != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(headerAddress);
|
||||
}
|
||||
|
||||
if (dataAddress != IntPtr.Zero)
|
||||
{
|
||||
Marshal.FreeHGlobal(dataAddress);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void ReapCompletedBuffers()
|
||||
{
|
||||
while (_buffers.TryPeek(out var buffer))
|
||||
{
|
||||
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
|
||||
if ((header.Flags & WaveHeaderDone) == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_buffers.Dequeue();
|
||||
ReleaseBuffer(buffer);
|
||||
}
|
||||
}
|
||||
|
||||
private void ReleaseBuffer(NativeBuffer buffer)
|
||||
{
|
||||
WaveOutUnprepareHeader(
|
||||
_device,
|
||||
buffer.Header,
|
||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
||||
_queuedPcmBytes -= buffer.Length;
|
||||
Marshal.FreeHGlobal(buffer.Header);
|
||||
Marshal.FreeHGlobal(buffer.Data);
|
||||
}
|
||||
|
||||
private static void ConvertToStereoPcm16(
|
||||
ReadOnlySpan<byte> source,
|
||||
Span<byte> destination,
|
||||
int frames,
|
||||
int channels,
|
||||
int bytesPerSample,
|
||||
bool isFloat)
|
||||
{
|
||||
var sourceFrameSize = checked(channels * bytesPerSample);
|
||||
for (var frame = 0; frame < frames; frame++)
|
||||
{
|
||||
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
|
||||
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
|
||||
var right = channels == 1
|
||||
? left
|
||||
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
|
||||
}
|
||||
}
|
||||
|
||||
private static short ReadSample(
|
||||
ReadOnlySpan<byte> frame,
|
||||
int channel,
|
||||
int bytesPerSample,
|
||||
bool isFloat)
|
||||
{
|
||||
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
|
||||
if (!isFloat)
|
||||
{
|
||||
return BinaryPrimitives.ReadInt16LittleEndian(sample);
|
||||
}
|
||||
|
||||
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
||||
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
||||
return checked((short)MathF.Round(value * short.MaxValue));
|
||||
}
|
||||
|
||||
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
||||
private struct WaveFormat
|
||||
{
|
||||
public ushort FormatTag;
|
||||
public ushort Channels;
|
||||
public uint SamplesPerSecond;
|
||||
public uint AverageBytesPerSecond;
|
||||
public ushort BlockAlign;
|
||||
public ushort BitsPerSample;
|
||||
public ushort ExtraSize;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct WaveHeader
|
||||
{
|
||||
public IntPtr Data;
|
||||
public uint BufferLength;
|
||||
public uint BytesRecorded;
|
||||
public nuint User;
|
||||
public uint Flags;
|
||||
public uint Loops;
|
||||
public IntPtr Next;
|
||||
public nuint Reserved;
|
||||
}
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
|
||||
private static extern uint WaveOutOpen(
|
||||
out IntPtr device,
|
||||
uint deviceId,
|
||||
ref WaveFormat format,
|
||||
IntPtr callback,
|
||||
IntPtr instance,
|
||||
uint flags);
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
|
||||
private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
|
||||
private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
|
||||
private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
|
||||
private static extern uint WaveOutReset(IntPtr device);
|
||||
|
||||
[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
|
||||
private static extern uint WaveOutClose(IntPtr device);
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
|
||||
namespace SharpEmu.Libs.AvPlayer;
|
||||
|
||||
public static class AvPlayerExports
|
||||
{
|
||||
private const int InvalidParameters = unchecked((int)0x806A0001);
|
||||
private static readonly object StateGate = new();
|
||||
private static readonly HashSet<ulong> Players = new();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "aS66RI0gGgo",
|
||||
ExportName = "sceAvPlayerInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerInit(CpuContext ctx)
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] == 0 ||
|
||||
!KernelMemoryCompatExports.TryAllocateHleData(ctx, 0x40, 16, out var handle))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
lock (StateGate)
|
||||
{
|
||||
Players.Add(handle);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = handle;
|
||||
return unchecked((int)handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "HD1YKVU26-M",
|
||||
ExportName = "sceAvPlayerPostInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerPostInit(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
lock (StateGate)
|
||||
{
|
||||
return ctx.SetReturn(
|
||||
handle != 0 && dataAddress != 0 && Players.Contains(handle)
|
||||
? 0
|
||||
: InvalidParameters);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "NkJwDzKmIlw",
|
||||
ExportName = "sceAvPlayerClose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerClose(CpuContext ctx)
|
||||
{
|
||||
lock (StateGate)
|
||||
{
|
||||
return ctx.SetReturn(Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.CommonDialog;
|
||||
|
||||
public static class MsgDialogExports
|
||||
{
|
||||
private const int AlreadyInitialized = unchecked((int)0x80B80002);
|
||||
private static int _initialized;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lDqxaY1UbEo",
|
||||
ExportName = "sceMsgDialogInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
||||
? 0
|
||||
: AlreadyInitialized;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,958 @@
|
||||
// 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.Fiber;
|
||||
|
||||
public static class FiberExports
|
||||
{
|
||||
private const int MaxNameLength = 31;
|
||||
private const int FiberInfoSize = 128;
|
||||
private const int FiberContextMinimumSize = 512;
|
||||
private const uint FiberSignature0 = 0xDEF1649C;
|
||||
private const uint FiberSignature1 = 0xB37592A0;
|
||||
private const uint FiberOptSignature = 0xBB40E64D;
|
||||
private const ulong FiberStackSignature = 0x7149F2CA7149F2CAUL;
|
||||
private const ulong FiberStackSizeCheck = 0xDEADBEEFDEADBEEFUL;
|
||||
private const uint FiberStateRun = 1;
|
||||
private const uint FiberStateIdle = 2;
|
||||
private const uint FiberStateTerminated = 3;
|
||||
private const uint FiberFlagContextSizeCheck = 0x10;
|
||||
|
||||
private const int FiberErrorNull = unchecked((int)0x80590001);
|
||||
private const int FiberErrorAlignment = unchecked((int)0x80590002);
|
||||
private const int FiberErrorRange = unchecked((int)0x80590003);
|
||||
private const int FiberErrorInvalid = unchecked((int)0x80590004);
|
||||
private const int FiberErrorPermission = unchecked((int)0x80590005);
|
||||
private const int FiberErrorState = unchecked((int)0x80590006);
|
||||
|
||||
private const int FiberMagicStartOffset = 0;
|
||||
private const int FiberStateOffset = 4;
|
||||
private const int FiberEntryOffset = 8;
|
||||
private const int FiberArgOnInitializeOffset = 16;
|
||||
private const int FiberContextAddressOffset = 24;
|
||||
private const int FiberContextSizeOffset = 32;
|
||||
private const int FiberNameOffset = 40;
|
||||
private const int FiberContextPointerOffset = 72;
|
||||
private const int FiberFlagsOffset = 80;
|
||||
private const int FiberContextStartOffset = 88;
|
||||
private const int FiberContextEndOffset = 96;
|
||||
private const int FiberMagicEndOffset = 104;
|
||||
|
||||
private static int _contextSizeCheck;
|
||||
|
||||
[ThreadStatic]
|
||||
private static ulong _currentFiberAddress;
|
||||
|
||||
private static readonly object _fiberGate = new();
|
||||
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
|
||||
private static readonly ConcurrentDictionary<ulong, FiberReturnTarget> _returnTargets = new();
|
||||
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hVYD7Ou2pCQ",
|
||||
ExportName = "_sceFiberInitializeImpl",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberInitialize(CpuContext ctx)
|
||||
{
|
||||
var optParam = ReadStackArg64(ctx, 0);
|
||||
return FiberInitializeCore(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi],
|
||||
ctx[CpuRegister.Rsi],
|
||||
ctx[CpuRegister.Rdx],
|
||||
ctx[CpuRegister.Rcx],
|
||||
ctx[CpuRegister.R8],
|
||||
ctx[CpuRegister.R9],
|
||||
optParam,
|
||||
flags: 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "7+OJIpko9RY",
|
||||
ExportName = "_sceFiberInitializeWithInternalOptionImpl",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberInitializeWithInternalOption(CpuContext ctx)
|
||||
{
|
||||
var optParam = ReadStackArg64(ctx, 0);
|
||||
var flags = unchecked((uint)ReadStackArg64(ctx, 1));
|
||||
return FiberInitializeCore(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi],
|
||||
ctx[CpuRegister.Rsi],
|
||||
ctx[CpuRegister.Rdx],
|
||||
ctx[CpuRegister.Rcx],
|
||||
ctx[CpuRegister.R8],
|
||||
ctx[CpuRegister.R9],
|
||||
optParam,
|
||||
flags);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "asjUJJ+aa8s",
|
||||
ExportName = "sceFiberOptParamInitialize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberOptParamInitialize(CpuContext ctx)
|
||||
{
|
||||
var optParam = ctx[CpuRegister.Rdi];
|
||||
if (optParam == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((optParam & 7) != 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JeNX5F-NzQU",
|
||||
ExportName = "sceFiberFinalize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberFinalize(CpuContext ctx)
|
||||
{
|
||||
var fiber = ctx[CpuRegister.Rdi];
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (state != FiberStateIdle)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations.TryRemove(fiber, out _);
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
_stackRanges.TryRemove(fiber, out _);
|
||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "a0LLrZWac0M",
|
||||
ExportName = "sceFiberRun",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberRun(CpuContext ctx)
|
||||
{
|
||||
return FiberRunCore(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi],
|
||||
ctx[CpuRegister.Rsi],
|
||||
ctx[CpuRegister.Rdx],
|
||||
attachContextAddress: 0,
|
||||
attachContextSize: 0,
|
||||
reason: "sceFiberRun",
|
||||
isSwitch: false);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "avfGJ94g36Q",
|
||||
ExportName = "_sceFiberAttachContextAndRun",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberAttachContextAndRun(CpuContext ctx)
|
||||
{
|
||||
return FiberRunCore(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi],
|
||||
ctx[CpuRegister.Rcx],
|
||||
ctx[CpuRegister.R8],
|
||||
attachContextAddress: ctx[CpuRegister.Rsi],
|
||||
attachContextSize: ctx[CpuRegister.Rdx],
|
||||
reason: "_sceFiberAttachContextAndRun",
|
||||
isSwitch: false);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "PFT2S-tJ7Uk",
|
||||
ExportName = "sceFiberSwitch",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberSwitch(CpuContext ctx)
|
||||
{
|
||||
return FiberRunCore(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi],
|
||||
ctx[CpuRegister.Rsi],
|
||||
ctx[CpuRegister.Rdx],
|
||||
attachContextAddress: 0,
|
||||
attachContextSize: 0,
|
||||
reason: "sceFiberSwitch",
|
||||
isSwitch: true);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ZqhZFuzKT6U",
|
||||
ExportName = "_sceFiberAttachContextAndSwitch",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberAttachContextAndSwitch(CpuContext ctx)
|
||||
{
|
||||
return FiberRunCore(
|
||||
ctx,
|
||||
ctx[CpuRegister.Rdi],
|
||||
ctx[CpuRegister.Rcx],
|
||||
ctx[CpuRegister.R8],
|
||||
attachContextAddress: ctx[CpuRegister.Rsi],
|
||||
attachContextSize: ctx[CpuRegister.Rdx],
|
||||
reason: "_sceFiberAttachContextAndSwitch",
|
||||
isSwitch: true);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "B0ZX2hx9DMw",
|
||||
ExportName = "sceFiberReturnToThread",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberReturnToThread(CpuContext ctx)
|
||||
{
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
var returnArgument = ctx[CpuRegister.Rdi];
|
||||
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
||||
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
ulong previousFiber;
|
||||
lock (_fiberGate)
|
||||
{
|
||||
_continuations[fiberAddress] = new FiberContinuation(
|
||||
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
||||
argOnRunAddress);
|
||||
|
||||
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
previousFiber = returnTarget.PreviousFiber;
|
||||
if (previousFiber != 0)
|
||||
{
|
||||
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
||||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = previousContinuation.Context with { Rax = 0 };
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!returnTarget.ThreadContinuation.HasValue ||
|
||||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
}
|
||||
|
||||
_currentFiberAddress = previousFiber;
|
||||
_ = GuestThreadExecution.EnterFiber(previousFiber);
|
||||
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||
TraceFiber(
|
||||
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
||||
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "p+zLIOg27zU",
|
||||
ExportName = "sceFiberGetSelf",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberGetSelf(CpuContext ctx)
|
||||
{
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uq2Y5BFz0PE",
|
||||
ExportName = "sceFiberGetInfo",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberGetInfo(CpuContext ctx)
|
||||
{
|
||||
var fiber = ctx[CpuRegister.Rdi];
|
||||
var info = ctx[CpuRegister.Rsi];
|
||||
if (info == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
||||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
|
||||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
|
||||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
|
||||
!TryWriteName(ctx, info + 40, fields.Name) ||
|
||||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JzyT91ucGDc",
|
||||
ExportName = "sceFiberRename",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberRename(CpuContext ctx)
|
||||
{
|
||||
var fiber = ctx[CpuRegister.Rdi];
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (nameAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Lcqty+QNWFc",
|
||||
ExportName = "sceFiberStartContextSizeCheck",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberStartContextSizeCheck(CpuContext ctx)
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] != 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Kj4nXMpnM8Y",
|
||||
ExportName = "sceFiberStopContextSizeCheck",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
||||
{
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "0dy4JtMUcMQ",
|
||||
ExportName = "_sceFiberGetThreadFramePointerAddress",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceFiber")]
|
||||
public static int FiberGetThreadFramePointerAddress(CpuContext ctx)
|
||||
{
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if (ResolveCurrentFiberAddress(ctx) == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
private static int FiberInitializeCore(
|
||||
CpuContext ctx,
|
||||
ulong fiber,
|
||||
ulong nameAddress,
|
||||
ulong entry,
|
||||
ulong argOnInitialize,
|
||||
ulong contextAddress,
|
||||
ulong contextSize,
|
||||
ulong optParam,
|
||||
uint flags)
|
||||
{
|
||||
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((fiber & 7) != 0 ||
|
||||
(contextAddress & 15) != 0 ||
|
||||
(optParam & 7) != 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
}
|
||||
|
||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorRange);
|
||||
}
|
||||
|
||||
if ((contextSize & 15) != 0 ||
|
||||
(contextAddress == 0 && contextSize != 0) ||
|
||||
(contextAddress != 0 && contextSize == 0))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (optParam != 0 &&
|
||||
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
||||
{
|
||||
flags |= FiberFlagContextSizeCheck;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (contextAddress != 0)
|
||||
{
|
||||
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if ((flags & FiberFlagContextSizeCheck) != 0)
|
||||
{
|
||||
FillContextSizeCheck(ctx, contextAddress + sizeof(ulong), contextSize - sizeof(ulong));
|
||||
}
|
||||
}
|
||||
|
||||
if (contextAddress != 0 && contextSize != 0)
|
||||
{
|
||||
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
|
||||
}
|
||||
|
||||
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static int FiberRunCore(
|
||||
CpuContext ctx,
|
||||
ulong fiber,
|
||||
ulong argOnRun,
|
||||
ulong outArgumentAddress,
|
||||
ulong attachContextAddress,
|
||||
ulong attachContextSize,
|
||||
string reason,
|
||||
bool isSwitch)
|
||||
{
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (attachContextAddress != 0 || attachContextSize != 0)
|
||||
{
|
||||
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
||||
if (attachResult != 0)
|
||||
{
|
||||
return ctx.SetReturn(attachResult);
|
||||
}
|
||||
}
|
||||
|
||||
var previousFiber = ResolveCurrentFiberAddress(ctx);
|
||||
if ((isSwitch && previousFiber == 0) ||
|
||||
(!isSwitch && previousFiber != 0))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
if (previousFiber == fiber)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
var resumed = false;
|
||||
lock (_fiberGate)
|
||||
{
|
||||
if (!TryReadFiberFields(ctx, fiber, out fields))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
if (fields.State != FiberStateIdle)
|
||||
{
|
||||
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
FiberContinuation targetContinuation;
|
||||
if (_continuations.TryGetValue(fiber, out var savedContinuation))
|
||||
{
|
||||
targetContinuation = savedContinuation;
|
||||
resumed = true;
|
||||
}
|
||||
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
var callerContinuation = new FiberContinuation(
|
||||
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
||||
outArgumentAddress);
|
||||
|
||||
if (previousFiber != 0)
|
||||
{
|
||||
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
|
||||
previousState != FiberStateRun ||
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations[previousFiber] = callerContinuation;
|
||||
_returnTargets[fiber] = new FiberReturnTarget(previousFiber, null);
|
||||
}
|
||||
else
|
||||
{
|
||||
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
|
||||
{
|
||||
if (previousFiber != 0)
|
||||
{
|
||||
_continuations.TryRemove(previousFiber, out _);
|
||||
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
||||
}
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed)
|
||||
{
|
||||
_continuations.TryRemove(fiber, out _);
|
||||
}
|
||||
|
||||
transferTarget = targetContinuation.Context with { Rax = 0 };
|
||||
}
|
||||
|
||||
_currentFiberAddress = fiber;
|
||||
_ = GuestThreadExecution.EnterFiber(fiber);
|
||||
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||
TraceFiber(
|
||||
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
||||
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryCreateInitialContinuation(
|
||||
CpuContext ctx,
|
||||
FiberFields fields,
|
||||
ulong argOnRun,
|
||||
out FiberContinuation continuation)
|
||||
{
|
||||
continuation = default;
|
||||
if (fields.ContextAddress == 0 || fields.ContextSize < FiberContextMinimumSize)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
||||
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
||||
if (!ctx.TryWriteUInt64(entryRsp, 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
continuation = new FiberContinuation(
|
||||
new GuestCpuContinuation(
|
||||
fields.Entry,
|
||||
entryRsp,
|
||||
entryRsp,
|
||||
ctx.Rflags == 0 ? 0x202UL : ctx.Rflags,
|
||||
ctx.FsBase,
|
||||
ctx.GsBase,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
argOnRun,
|
||||
fields.ArgOnInitialize,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0,
|
||||
0),
|
||||
0);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteResumeArgument(
|
||||
CpuContext ctx,
|
||||
FiberContinuation continuation,
|
||||
ulong argument) =>
|
||||
continuation.ArgOnRunAddress == 0 ||
|
||||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
|
||||
|
||||
private static GuestCpuContinuation CaptureContinuation(
|
||||
CpuContext ctx,
|
||||
ulong resumeRip,
|
||||
ulong resumeRsp,
|
||||
ulong returnSlotAddress) =>
|
||||
new(
|
||||
resumeRip,
|
||||
resumeRsp,
|
||||
returnSlotAddress,
|
||||
ctx.Rflags == 0 ? 0x202UL : ctx.Rflags,
|
||||
ctx.FsBase,
|
||||
ctx.GsBase,
|
||||
0,
|
||||
ctx[CpuRegister.Rcx],
|
||||
ctx[CpuRegister.Rdx],
|
||||
ctx[CpuRegister.Rbx],
|
||||
ctx[CpuRegister.Rbp],
|
||||
ctx[CpuRegister.Rsi],
|
||||
ctx[CpuRegister.Rdi],
|
||||
ctx[CpuRegister.R8],
|
||||
ctx[CpuRegister.R9],
|
||||
ctx[CpuRegister.R12],
|
||||
ctx[CpuRegister.R13],
|
||||
ctx[CpuRegister.R14],
|
||||
ctx[CpuRegister.R15]);
|
||||
|
||||
private static ulong ResolveCurrentFiberAddress(CpuContext ctx)
|
||||
{
|
||||
if (_currentFiberAddress != 0)
|
||||
{
|
||||
return _currentFiberAddress;
|
||||
}
|
||||
|
||||
if (GuestThreadExecution.CurrentFiberAddress != 0)
|
||||
{
|
||||
return GuestThreadExecution.CurrentFiberAddress;
|
||||
}
|
||||
|
||||
return TryFindFiberByStack(ctx, out var fiberAddress) ? fiberAddress : 0;
|
||||
}
|
||||
|
||||
internal static ulong GetCurrentFiberAddressForDiagnostics(CpuContext ctx) =>
|
||||
ResolveCurrentFiberAddress(ctx);
|
||||
|
||||
private static int AttachContext(
|
||||
CpuContext ctx,
|
||||
ulong fiber,
|
||||
ulong contextAddress,
|
||||
ulong contextSize,
|
||||
ref FiberFields fields)
|
||||
{
|
||||
if ((contextAddress & 15) != 0)
|
||||
{
|
||||
return FiberErrorAlignment;
|
||||
}
|
||||
|
||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||
{
|
||||
return FiberErrorRange;
|
||||
}
|
||||
|
||||
if ((contextSize & 15) != 0 ||
|
||||
contextAddress == 0 ||
|
||||
contextSize == 0 ||
|
||||
fields.ContextAddress != 0)
|
||||
{
|
||||
return FiberErrorInvalid;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||
{
|
||||
return FiberErrorInvalid;
|
||||
}
|
||||
|
||||
fields = fields with
|
||||
{
|
||||
ContextAddress = contextAddress,
|
||||
ContextSize = contextSize,
|
||||
};
|
||||
_stackRanges[fiber] = new FiberStackRange(contextAddress, contextSize);
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool TryFindFiberByStack(CpuContext ctx, out ulong fiber)
|
||||
{
|
||||
if (TryFindFiberByStackAddress(ctx[CpuRegister.Rsp], out fiber))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return TryFindFiberByStackAddress(ctx[CpuRegister.Rbp], out fiber);
|
||||
}
|
||||
|
||||
private static bool TryFindFiberByStackAddress(ulong address, out ulong fiber)
|
||||
{
|
||||
if (address != 0)
|
||||
{
|
||||
foreach (var (candidate, range) in _stackRanges)
|
||||
{
|
||||
if (range.Contains(address))
|
||||
{
|
||||
fiber = candidate;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fiber = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool TryValidateFiber(CpuContext ctx, ulong fiber, out int error)
|
||||
{
|
||||
if (fiber == 0)
|
||||
{
|
||||
error = FiberErrorNull;
|
||||
return false;
|
||||
}
|
||||
|
||||
if ((fiber & 7) != 0)
|
||||
{
|
||||
error = FiberErrorAlignment;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||
magicStart != FiberSignature0 ||
|
||||
magicEnd != FiberSignature1)
|
||||
{
|
||||
error = FiberErrorInvalid;
|
||||
return false;
|
||||
}
|
||||
|
||||
error = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
||||
{
|
||||
fields = default;
|
||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
|
||||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
fields = new FiberFields(
|
||||
state,
|
||||
entry,
|
||||
argOnInitialize,
|
||||
contextAddress,
|
||||
contextSize,
|
||||
flags,
|
||||
name);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void FillContextSizeCheck(CpuContext ctx, ulong address, ulong size)
|
||||
{
|
||||
Span<byte> value = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(value, FiberStackSizeCheck);
|
||||
var end = address + size;
|
||||
for (var current = address; current + sizeof(ulong) <= end; current += sizeof(ulong))
|
||||
{
|
||||
_ = ctx.Memory.TryWrite(current, value);
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong ReadStackArg64(CpuContext ctx, int index)
|
||||
{
|
||||
if (ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong) + ((ulong)index * sizeof(ulong)), out var value))
|
||||
{
|
||||
return value;
|
||||
}
|
||||
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
||||
var bytes = Encoding.UTF8.GetBytes(name);
|
||||
bytes.AsSpan(0, Math.Min(bytes.Length, MaxNameLength)).CopyTo(buffer);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryReadInlineName(CpuContext ctx, ulong address, out string value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
var length = buffer.IndexOf((byte)0);
|
||||
if (length < 0)
|
||||
{
|
||||
length = buffer.Length;
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(buffer[..length]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceFiber(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] fiber.{message}");
|
||||
}
|
||||
}
|
||||
|
||||
private readonly record struct FiberFields(
|
||||
uint State,
|
||||
ulong Entry,
|
||||
ulong ArgOnInitialize,
|
||||
ulong ContextAddress,
|
||||
ulong ContextSize,
|
||||
uint Flags,
|
||||
string Name);
|
||||
|
||||
private readonly record struct FiberContinuation(
|
||||
GuestCpuContinuation Context,
|
||||
ulong ArgOnRunAddress);
|
||||
|
||||
private readonly record struct FiberReturnTarget(
|
||||
ulong PreviousFiber,
|
||||
FiberContinuation? ThreadContinuation);
|
||||
|
||||
private readonly record struct FiberStackRange(ulong Start, ulong Size)
|
||||
{
|
||||
public bool Contains(ulong address) =>
|
||||
Size != 0 && address >= Start && address < Start + Size;
|
||||
}
|
||||
}
|
||||
@@ -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,7 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using SharpEmu.Libs.Ampr;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
@@ -10,8 +10,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,9 +41,15 @@ public static class KernelAprCompatExports
|
||||
submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
||||
}
|
||||
|
||||
_submittedCommandBuffers[submissionId] = commandBuffer;
|
||||
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, priority, resultAddress);
|
||||
|
||||
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
||||
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -60,20 +71,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 +105,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,9 +132,15 @@ 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);
|
||||
|
||||
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
||||
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -130,11 +156,25 @@ public static class KernelAprCompatExports
|
||||
return ctx.Memory.TryWrite(resultAddress, result);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
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 void TraceApr(
|
||||
@@ -145,7 +185,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 +194,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,559 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
||||
{
|
||||
return ctx.SetReturn(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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
|
||||
return ctx.SetReturn(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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
{
|
||||
Monitor.PulseAll(state.Gate);
|
||||
}
|
||||
|
||||
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||
return ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||
{
|
||||
return ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
uint timeoutUsec = 0;
|
||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Monitor.Enter(state.Gate);
|
||||
try
|
||||
{
|
||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
||||
{
|
||||
return ctx.SetReturn(immediateWaitResult);
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = ctx.TryWriteUInt32(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 ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (waiterCountAddress != 0 &&
|
||||
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||
{
|
||||
return ctx.SetReturn(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 ctx.SetReturn(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 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 (ctx.TryReadByte(address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X2}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
if (ctx.TryReadUInt32(address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X8}");
|
||||
}
|
||||
}
|
||||
|
||||
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
if (ctx.TryReadUInt64(address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X16}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -10,10 +10,14 @@ 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, Queue<KernelQueuedEvent>> _pendingEvents = new();
|
||||
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
|
||||
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
|
||||
private static long _nextEventQueueHandle = 1;
|
||||
|
||||
public readonly record struct KernelQueuedEvent(
|
||||
@@ -24,6 +28,11 @@ public static class KernelEventQueueCompatExports
|
||||
ulong Data,
|
||||
ulong UserData);
|
||||
|
||||
private readonly record struct KernelEventRegistration(
|
||||
ulong Ident,
|
||||
short Filter,
|
||||
ulong UserData);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "D0OdFMjp46I",
|
||||
ExportName = "sceKernelCreateEqueue",
|
||||
@@ -41,7 +50,8 @@ public static class KernelEventQueueCompatExports
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
_eventQueues.Add(handle);
|
||||
_pendingEvents[handle] = new Queue<KernelQueuedEvent>();
|
||||
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
|
||||
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||
@@ -65,6 +75,7 @@ public static class KernelEventQueueCompatExports
|
||||
{
|
||||
_eventQueues.Remove(handle);
|
||||
_pendingEvents.Remove(handle);
|
||||
_registeredEvents.Remove(handle);
|
||||
}
|
||||
|
||||
TraceEventQueue(ctx, "delete", handle);
|
||||
@@ -122,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(
|
||||
@@ -133,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(
|
||||
@@ -144,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(
|
||||
@@ -155,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(
|
||||
@@ -171,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",
|
||||
@@ -184,8 +276,24 @@ public static class KernelEventQueueCompatExports
|
||||
var outCountAddress = ctx[CpuRegister.Rcx];
|
||||
var timeoutAddress = ctx[CpuRegister.R8];
|
||||
|
||||
if (!IsValidEqueue(handle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (eventsAddress == 0 || eventCapacity < 1)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
uint timeoutUsec = 0;
|
||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -196,12 +304,53 @@ public static class KernelEventQueueCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue"))
|
||||
if (timeoutAddress == 0 &&
|
||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitEqueue",
|
||||
GetEventQueueWakeKey(handle),
|
||||
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress),
|
||||
() => HasPendingEvents(handle)))
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-block", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0 && ctx.TryReadUInt64(timeoutAddress, out var timeoutRaw))
|
||||
{
|
||||
var timeoutMicros = timeoutRaw & 0xFFFF_FFFFUL;
|
||||
var deadline = Environment.TickCount64 +
|
||||
Math.Max(1L, (long)Math.Min(timeoutMicros / 1000, int.MaxValue));
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
while (!HasPendingEvents(handle))
|
||||
{
|
||||
var remaining = deadline - Environment.TickCount64;
|
||||
if (remaining <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Monitor.Wait(_eventQueueGate, (int)Math.Min(remaining, 100));
|
||||
}
|
||||
}
|
||||
|
||||
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (deliveredCount > 0)
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-timed-deliver", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
TraceEventQueue(ctx, "wait-timeout", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
}
|
||||
|
||||
TraceEventQueue(ctx, "wait", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -216,6 +365,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
|
||||
{
|
||||
var queued = false;
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
if (!_eventQueues.Contains(handle))
|
||||
@@ -225,15 +375,231 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||
{
|
||||
queue = new Queue<KernelQueuedEvent>();
|
||||
queue = new LinkedList<KernelQueuedEvent>();
|
||||
_pendingEvents[handle] = queue;
|
||||
}
|
||||
|
||||
queue.Enqueue(queuedEvent);
|
||||
queue.AddLast(queuedEvent);
|
||||
queued = true;
|
||||
Monitor.PulseAll(_eventQueueGate);
|
||||
}
|
||||
|
||||
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 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
return deliveredCount > 0
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -253,7 +619,8 @@ public static class KernelEventQueueCompatExports
|
||||
events = new KernelQueuedEvent[count];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
events[i] = queue.Dequeue();
|
||||
events[i] = queue.First!.Value;
|
||||
queue.RemoveFirst();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -287,16 +654,8 @@ public static class KernelEventQueueCompatExports
|
||||
return;
|
||||
}
|
||||
|
||||
var returnRip = 0UL;
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,65 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelExceptionCompatExports
|
||||
{
|
||||
private static readonly HashSet<int> AllowedSignals = new() { 1, 4, 8, 10, 11, 30 };
|
||||
private static readonly Dictionary<int, ulong> _installedHandlers = new();
|
||||
private static readonly object _gate = new();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "WkwEd3N7w0Y",
|
||||
ExportName = "sceKernelInstallExceptionHandler",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int InstallExceptionHandler(CpuContext ctx)
|
||||
{
|
||||
var signum = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var handler = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (!AllowedSignals.Contains(signum))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
if (_installedHandlers.ContainsKey(signum))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
|
||||
}
|
||||
|
||||
_installedHandlers[signum] = handler;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Qhv5ARAoOEc",
|
||||
ExportName = "sceKernelRemoveExceptionHandler",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int RemoveExceptionHandler(CpuContext ctx)
|
||||
{
|
||||
var signum = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
|
||||
if (!AllowedSignals.Contains(signum))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
{
|
||||
_installedHandlers.Remove(signum);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
@@ -8,7 +8,6 @@ namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelExports
|
||||
{
|
||||
private static int _nextFileDescriptor = 2;
|
||||
private static readonly object _cxaGate = new();
|
||||
private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
|
||||
private static readonly object _coredumpGate = new();
|
||||
@@ -204,6 +203,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 +221,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 +270,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 +305,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 +312,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;
|
||||
}
|
||||
@@ -300,6 +364,13 @@ public static class KernelExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "mqQMh1zPPT8",
|
||||
ExportName = "fstat",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.KernelFstat(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hcuQgD53UxM",
|
||||
ExportName = "printf",
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,43 +1,56 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using System.Threading;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
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 +73,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",
|
||||
@@ -73,6 +93,13 @@ public static class KernelPthreadCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "7Xl257M4VNI",
|
||||
ExportName = "pthread_equal",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int PosixPthreadEqual(CpuContext ctx) => PthreadEqual(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "T72hz6ffq08",
|
||||
ExportName = "scePthreadYield",
|
||||
@@ -300,6 +327,12 @@ public static class KernelPthreadCompatExports
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
||||
[SysAbiExport(
|
||||
Nid = "27bAgiJmOh0",
|
||||
ExportName = "pthread_cond_timedwait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "m5-2bsNfv7s",
|
||||
@@ -343,6 +376,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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadInt32(onceAddress, out var onceValue))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (onceValue == PthreadOnceDone)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
var gate = GetPthreadOnceGate(onceAddress);
|
||||
var shouldCall = false;
|
||||
lock (gate)
|
||||
{
|
||||
if (!ctx.TryReadInt32(onceAddress, out onceValue))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
while (onceValue == PthreadOnceInProgress)
|
||||
{
|
||||
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
|
||||
if (!ctx.TryReadInt32(onceAddress, out onceValue))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (onceValue != PthreadOnceDone)
|
||||
{
|
||||
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceInProgress))
|
||||
{
|
||||
return ctx.SetReturn(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)
|
||||
{
|
||||
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
}
|
||||
|
||||
TracePthreadOnce(onceAddress, initRoutine, "failed", error);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
lock (gate)
|
||||
{
|
||||
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceDone))
|
||||
{
|
||||
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Monitor.PulseAll(gate);
|
||||
}
|
||||
}
|
||||
|
||||
TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||
{
|
||||
if (mutexAddress == 0)
|
||||
@@ -357,14 +477,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 +510,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 +521,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 +576,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 +661,7 @@ public static class KernelPthreadCompatExports
|
||||
try
|
||||
{
|
||||
state.Semaphore.Release();
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
|
||||
}
|
||||
catch (SemaphoreFullException)
|
||||
{
|
||||
@@ -533,14 +681,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 +740,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 +769,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 +797,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 +822,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 +840,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 +857,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 +867,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 +878,14 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
lock (_stateGate)
|
||||
{
|
||||
if (_mutexAttrStates.ContainsKey(attrAddress))
|
||||
{
|
||||
return attrAddress;
|
||||
}
|
||||
}
|
||||
|
||||
return attrAddress;
|
||||
}
|
||||
|
||||
@@ -737,7 +901,7 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
return _mutexAttrStates.TryGetValue(resolvedAddress, out var state)
|
||||
? state
|
||||
: new PthreadMutexAttrState(MutexTypeDefault, 0);
|
||||
: new PthreadMutexAttrState(MutexTypeErrorCheck, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -756,7 +920,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 +952,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,25 +980,55 @@ 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) =>
|
||||
ctx.TryWriteUInt32(address + 0x20, unchecked((uint)state.Type)) &&
|
||||
ctx.TryWriteUInt32(address + 0x3C, unchecked((uint)state.Protocol));
|
||||
|
||||
private static bool WriteMutexAttrObject(CpuContext ctx, ulong address, PthreadMutexAttrState state) =>
|
||||
ctx.TryWriteUInt32(address, unchecked((uint)state.Type)) &&
|
||||
ctx.TryWriteUInt32(address + 4, unchecked((uint)state.Protocol));
|
||||
|
||||
private static int PthreadCondInitCore(CpuContext ctx, ulong condAddress)
|
||||
{
|
||||
if (condAddress == 0)
|
||||
@@ -842,15 +1036,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 +1079,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt64(condAddress, 0);
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, condAddress, 0);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1007,6 +1215,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 +1296,29 @@ 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 bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
|
||||
{
|
||||
var createdState = new PthreadMutexState
|
||||
@@ -1047,7 +1326,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 +1347,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 +1384,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 +1414,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 +1426,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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,9 +2,8 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -12,26 +11,70 @@ 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;
|
||||
private const ulong SyntheticRwlockAttrHandleBase = 0x00006005_0000_0000;
|
||||
|
||||
private static readonly object _stateGate = new();
|
||||
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
|
||||
private static readonly Dictionary<ulong, PthreadAttrState> _attrStates = new();
|
||||
private static readonly Dictionary<ulong, 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 long _nextSyntheticRwlockAttrHandleId = 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",
|
||||
@@ -205,7 +277,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
||||
}
|
||||
|
||||
if (!TryWriteInt32(ctx, outPriorityAddress, priority))
|
||||
if (!ctx.TryWriteInt32(outPriorityAddress, priority))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -252,7 +324,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
|
||||
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -422,7 +494,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||
}
|
||||
|
||||
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
|
||||
if (!ctx.TryWriteInt32(outStateAddress, state.DetachState))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -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",
|
||||
@@ -614,6 +717,35 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "FXPWHNk8Of0",
|
||||
ExportName = "scePthreadAttrGetschedparam",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrGetschedparam(CpuContext ctx)
|
||||
{
|
||||
var attrAddress = ctx[CpuRegister.Rdi];
|
||||
var schedParamAddress = ctx[CpuRegister.Rsi];
|
||||
if (attrAddress == 0 || schedParamAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
PthreadAttrState state;
|
||||
lock (_stateGate)
|
||||
{
|
||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteInt32(schedParamAddress, state.SchedPriority))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "DzES9hQF4f4",
|
||||
ExportName = "scePthreadAttrSetschedparam",
|
||||
@@ -628,7 +760,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
|
||||
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -707,6 +839,37 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return PthreadAttrSetstacksize(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Bvn74vj6oLo",
|
||||
ExportName = "scePthreadAttrSetstack",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrSetstack(CpuContext ctx)
|
||||
{
|
||||
var attrAddress = ctx[CpuRegister.Rdi];
|
||||
var stackAddress = ctx[CpuRegister.Rsi];
|
||||
var stackSize = ctx[CpuRegister.Rdx];
|
||||
if (attrAddress == 0 || stackAddress == 0 || stackSize == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
|
||||
lock (_stateGate)
|
||||
{
|
||||
var state = GetOrCreateAttrStateLocked(resolvedAddress);
|
||||
var updated = state with { StackAddress = stackAddress, StackSize = stackSize };
|
||||
_attrStates[resolvedAddress] = updated;
|
||||
if (resolvedAddress != attrAddress)
|
||||
{
|
||||
_attrStates[attrAddress] = updated;
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "6ULAa0fq4jA",
|
||||
ExportName = "scePthreadRwlockInit",
|
||||
@@ -733,7 +896,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 +950,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt64(rwlockAddress, 0);
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, 0);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -884,6 +1047,47 @@ public static class KernelPthreadExtendedCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadRwlockUnlock(CpuContext ctx) => PthreadRwlockUnlock(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yOfGg-I1ZII",
|
||||
ExportName = "scePthreadRwlockattrInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadRwlockattrInit(CpuContext ctx)
|
||||
{
|
||||
var attrAddress = ctx[CpuRegister.Rdi];
|
||||
if (attrAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var syntheticHandle = AllocateSyntheticHandle(SyntheticRwlockAttrHandleBase, ref _nextSyntheticRwlockAttrHandleId);
|
||||
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, syntheticHandle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "i2ifZ3fS2fo",
|
||||
ExportName = "scePthreadRwlockattrDestroy",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadRwlockattrDestroy(CpuContext ctx)
|
||||
{
|
||||
var attrAddress = ctx[CpuRegister.Rdi];
|
||||
if (attrAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, 0);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "mqULNdimTn0",
|
||||
ExportName = "pthread_key_create",
|
||||
@@ -899,23 +1103,21 @@ 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))
|
||||
if (!ctx.TryWriteInt32(outKeyAddress, key))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((uint)key);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -934,17 +1136,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 +1167,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 +1197,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 +1292,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 +1324,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(rwlockAddress, out var pointedHandle))
|
||||
if (!KernelMemoryCompatExports.TryReadUInt64Compat(ctx, rwlockAddress, out var pointedHandle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1170,7 +1359,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
_rwlockStates[syntheticHandle] = createdRwlock;
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt64(rwlockAddress, syntheticHandle);
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, rwlockAddress, syntheticHandle);
|
||||
resolvedAddress = syntheticHandle;
|
||||
rwlock = createdRwlock;
|
||||
return true;
|
||||
@@ -1260,24 +1449,4 @@ public static class KernelPthreadExtendedCompatExports
|
||||
payload[^1] = 0;
|
||||
return ctx.Memory.TryWrite(address, payload);
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,16 +2,13 @@
|
||||
// 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;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
@@ -25,12 +22,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 +52,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 +72,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. Do not turn
|
||||
// PS microsecond waits into Windows millisecond sleeps on hot paths.
|
||||
if ((++_shortUsleepCount & 255) == 0)
|
||||
{
|
||||
Thread.SpinWait(16);
|
||||
Thread.Sleep(0);
|
||||
}
|
||||
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 +235,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 &&
|
||||
(!ctx.TryWriteInt32(timezoneAddress, 0) ||
|
||||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), 0)))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "-2IRUCO--PM",
|
||||
ExportName = "sceKernelReadTsc",
|
||||
@@ -497,19 +607,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 && ctx.TryWriteInt32(address, value);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bnZxYgAFeA0",
|
||||
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
|
||||
@@ -552,6 +659,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 +708,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 +720,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 +730,16 @@ 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}");
|
||||
if (ShouldTraceVirtualMemory())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[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))
|
||||
{
|
||||
@@ -629,6 +751,7 @@ public static class KernelRuntimeCompatExports
|
||||
_nextReservedVirtualBase = Math.Max(_nextReservedVirtualBase, mappedAddress + length);
|
||||
}
|
||||
|
||||
KernelMemoryCompatExports.RegisterReservedVirtualRange(mappedAddress, length);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -701,7 +824,7 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
|
||||
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
||||
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -731,7 +854,7 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
|
||||
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
||||
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -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(
|
||||
@@ -934,7 +1055,7 @@ public static class KernelRuntimeCompatExports
|
||||
public static int KernelStopUnloadModule(CpuContext ctx)
|
||||
{
|
||||
var resultAddress = ctx[CpuRegister.R9];
|
||||
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0))
|
||||
if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -952,7 +1073,7 @@ public static class KernelRuntimeCompatExports
|
||||
{
|
||||
var modulePathAddress = ctx[CpuRegister.Rdi];
|
||||
var resultAddress = ctx[CpuRegister.R9];
|
||||
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0))
|
||||
if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -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 (!ctx.TryWriteInt32(timezoneAddress, minutesWest) ||
|
||||
!ctx.TryWriteInt32(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 && !ctx.TryWriteInt32(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",
|
||||
@@ -1075,7 +1301,7 @@ public static class KernelRuntimeCompatExports
|
||||
_loadedSysmodules.Add(moduleId);
|
||||
}
|
||||
|
||||
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0))
|
||||
if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1165,7 +1391,7 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, module.Handle))
|
||||
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, module.Handle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1202,7 +1428,7 @@ public static class KernelRuntimeCompatExports
|
||||
var writableCount = (int)Math.Min(Math.Min(capacity, (ulong)int.MaxValue), (ulong)handles.Length);
|
||||
for (var i = 0; i < writableCount; i++)
|
||||
{
|
||||
if (!TryWriteInt32(ctx, handlesAddress + (ulong)(i * sizeof(int)), handles[i]))
|
||||
if (!ctx.TryWriteInt32(handlesAddress + (ulong)(i * sizeof(int)), handles[i]))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1275,13 +1501,6 @@ public static class KernelRuntimeCompatExports
|
||||
return unchecked(ctx.FsBase + offset);
|
||||
}
|
||||
|
||||
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 nint AllocateStackChkGuardObject()
|
||||
{
|
||||
try
|
||||
@@ -1306,30 +1525,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,96 +1717,24 @@ 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)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object memoryObject = ctx.Memory;
|
||||
MethodInfo? allocateAt = 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 &&
|
||||
parameters[0].ParameterType == typeof(ulong) &&
|
||||
parameters[1].ParameterType == typeof(ulong) &&
|
||||
parameters[2].ParameterType == typeof(bool) &&
|
||||
parameters[3].ParameterType == typeof(bool))
|
||||
{
|
||||
allocateAt = candidate;
|
||||
allocateAtHasAllowAlternativeArg = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocateAt is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var innerProperty = memoryObject.GetType().GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance);
|
||||
if (innerProperty is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var innerValue = innerProperty.GetValue(memoryObject);
|
||||
if (innerValue is null || ReferenceEquals(innerValue, memoryObject))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
memoryObject = innerValue;
|
||||
}
|
||||
|
||||
if (allocateAt is null)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var invokeArgs = allocateAtHasAllowAlternativeArg
|
||||
? new object[] { desiredAddress, length, false, true }
|
||||
: new object[] { desiredAddress, length, false };
|
||||
var result = allocateAt.Invoke(memoryObject, invokeArgs);
|
||||
if (result is not ulong allocated || allocated == 0)
|
||||
{
|
||||
var resultType = result?.GetType().FullName ?? "null";
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt returned {resultType} value={result ?? "null"}");
|
||||
return false;
|
||||
}
|
||||
|
||||
mappedAddress = allocated;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][TRACE] reserve_virtual_range: AllocateAt invocation threw");
|
||||
return false;
|
||||
}
|
||||
return KernelVirtualRangeAllocator.TryReserve(
|
||||
ctx,
|
||||
desiredAddress,
|
||||
length,
|
||||
executable: false,
|
||||
alignment,
|
||||
allowSearch,
|
||||
allowAllocateAtAlternative: allowSearch,
|
||||
"reserve_virtual_range",
|
||||
out mappedAddress);
|
||||
}
|
||||
|
||||
private static ulong AlignUp(ulong value, ulong alignment)
|
||||
@@ -1590,4 +1747,9 @@ public static class KernelRuntimeCompatExports
|
||||
var mask = alignment - 1;
|
||||
return (value + mask) & ~mask;
|
||||
}
|
||||
|
||||
private static bool ShouldTraceVirtualMemory()
|
||||
{
|
||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIRTUAL_MEMORY"), "1", StringComparison.Ordinal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// 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;
|
||||
@@ -46,12 +44,12 @@ public static class KernelSemaphoreCompatExports
|
||||
initialCount > maxCount ||
|
||||
optionAddress != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||
@@ -68,14 +66,14 @@ public static class KernelSemaphoreCompatExports
|
||||
Count = initialCount,
|
||||
};
|
||||
|
||||
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
||||
{
|
||||
_semaphores.TryRemove(handle, out _);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(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);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -91,12 +89,12 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
@@ -105,30 +103,30 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
if (!TryReadUInt32(ctx, timeoutAddress, out _))
|
||||
if (!ctx.TryReadUInt32(timeoutAddress, out _))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
_ = ctx.TryWriteUInt32(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);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitSema"))
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
return ctx.SetReturn(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);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,12 +142,12 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
@@ -157,12 +155,12 @@ public static class KernelSemaphoreCompatExports
|
||||
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);
|
||||
return ctx.SetReturn(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);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -178,24 +176,24 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (signalCount <= 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count > semaphore.MaxCount - signalCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(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);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -212,25 +210,25 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (setCount > semaphore.MaxCount)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(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);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -244,68 +242,11 @@ public static class KernelSemaphoreCompatExports
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
if (!_semaphores.TryRemove(handle, out var semaphore))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(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;
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static void TraceSemaphore(string message)
|
||||
|
||||
@@ -0,0 +1,159 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
internal static class KernelVirtualRangeAllocator
|
||||
{
|
||||
private static readonly ConcurrentDictionary<Type, Accessor> _accessors = new();
|
||||
|
||||
public static bool TryReserve(
|
||||
CpuContext ctx,
|
||||
ulong desiredAddress,
|
||||
ulong length,
|
||||
bool executable,
|
||||
ulong alignment,
|
||||
bool allowSearch,
|
||||
bool allowAllocateAtAlternative,
|
||||
string traceName,
|
||||
out ulong mappedAddress)
|
||||
{
|
||||
mappedAddress = 0;
|
||||
if (length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (!TryResolveAccessor(ctx.Memory, out var target, out var accessor))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
if (allowSearch && accessor.AllocateAtOrAbove is not null)
|
||||
{
|
||||
var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL };
|
||||
var searchResult = accessor.AllocateAtOrAbove.Invoke(target, searchArgs);
|
||||
if (searchResult is bool trueValue && trueValue &&
|
||||
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
||||
{
|
||||
mappedAddress = searchedAddress;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (accessor.AllocateAt is null)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {target.GetType().FullName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var invokeArgs = accessor.AllocateAtHasAllowAlternativeArg
|
||||
? new object[] { desiredAddress, length, executable, allowAllocateAtAlternative }
|
||||
: new object[] { desiredAddress, length, executable };
|
||||
var result = accessor.AllocateAt.Invoke(target, invokeArgs);
|
||||
if (result is not ulong allocated || allocated == 0)
|
||||
{
|
||||
var resultType = result?.GetType().FullName ?? "null";
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {resultType} value={result ?? "null"}");
|
||||
return false;
|
||||
}
|
||||
|
||||
mappedAddress = allocated;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt invocation threw");
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryResolveAccessor(object rootMemory, out object target, out Accessor accessor)
|
||||
{
|
||||
target = rootMemory;
|
||||
accessor = default;
|
||||
|
||||
for (var depth = 0; depth < 4; depth++)
|
||||
{
|
||||
accessor = _accessors.GetOrAdd(target.GetType(), DiscoverAccessor);
|
||||
if (accessor.AllocateAt is not null || accessor.AllocateAtOrAbove is not null)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
if (accessor.InnerProperty is null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var innerValue = accessor.InnerProperty.GetValue(target);
|
||||
if (innerValue is null || ReferenceEquals(innerValue, target))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
target = innerValue;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static Accessor DiscoverAccessor(Type type)
|
||||
{
|
||||
MethodInfo? allocateAt = null;
|
||||
MethodInfo? allocateAtOrAbove = null;
|
||||
var allocateAtHasAllowAlternativeArg = false;
|
||||
|
||||
foreach (var candidate in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
var parameters = candidate.GetParameters();
|
||||
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(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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var innerProperty = type.GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance);
|
||||
return new Accessor(allocateAt, allocateAtOrAbove, allocateAtHasAllowAlternativeArg, innerProperty);
|
||||
}
|
||||
|
||||
private readonly record struct Accessor(
|
||||
MethodInfo? AllocateAt,
|
||||
MethodInfo? AllocateAtOrAbove,
|
||||
bool AllocateAtHasAllowAlternativeArg,
|
||||
PropertyInfo? InnerProperty);
|
||||
}
|
||||
@@ -0,0 +1,71 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
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 ctx.SetReturn(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 ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
TraceHttp2("term", id, 0, 0, 0, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
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,115 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
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 ctx.SetReturn(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 ctx.SetReturn(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 _)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(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 ctx.SetReturn(HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
foreach (var pair in Templates)
|
||||
{
|
||||
if (pair.Value.ContextId == contextId)
|
||||
{
|
||||
Templates.TryRemove(pair.Key, out _);
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
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,223 @@
|
||||
// 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 ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> natInfo = stackalloc byte[NatInfoSize];
|
||||
if (!ctx.Memory.TryRead(natInfoAddress, natInfo))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]);
|
||||
if (size != NatInfoSize)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, typeof(long));
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteInt32LittleEndian(natInfo[4..], 1);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(natInfo[8..], 3);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001);
|
||||
return ctx.Memory.TryWrite(natInfoAddress, natInfo)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "iQw3iQPhvUQ",
|
||||
ExportName = "sceNetCtlCheckCallback",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNetCtl")]
|
||||
public static int NetCtlCheckCallback(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
|
||||
[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 ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> stateBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0);
|
||||
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[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 ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
lock (CallbackGate)
|
||||
{
|
||||
var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0);
|
||||
if (callbackId < 0)
|
||||
{
|
||||
return ctx.SetReturn(NetCtlErrorNoSpace, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> callbackIdBytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId));
|
||||
if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
Callbacks[callbackId] = new CallbackRegistration(function, argument);
|
||||
}
|
||||
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
|
||||
}
|
||||
|
||||
[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 ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
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),
|
||||
_ => ctx.SetReturn(NetCtlErrorNotConnected, typeof(long)),
|
||||
};
|
||||
}
|
||||
|
||||
private static int WriteZeroBytes(CpuContext ctx, ulong address, int count)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[count];
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
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)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
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)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
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)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
// 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.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 ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||
}
|
||||
|
||||
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
|
||||
return ctx.SetReturn(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 ctx.SetReturn(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 _)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(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 ctx.SetReturn(NetErrorInvalidArgument);
|
||||
}
|
||||
|
||||
if (!_resolvers.TryGetValue(id, out var resolver))
|
||||
{
|
||||
return ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||
}
|
||||
|
||||
Span<byte> status = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
|
||||
return ctx.Memory.TryWrite(statusAddress, status)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
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,79 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
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 ctx.SetReturn(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 ctx.SetReturn(SslErrorInvalidId);
|
||||
}
|
||||
|
||||
TraceSsl("term", id, 0);
|
||||
return ctx.SetReturn(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 ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
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,316 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.Ngs2;
|
||||
|
||||
public static class Ngs2Exports
|
||||
{
|
||||
private const int OrbisNgs2ErrorInvalidOutAddress = unchecked((int)0x804A0053);
|
||||
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
|
||||
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
|
||||
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
|
||||
private const ulong HandleStorageSize = 0x20;
|
||||
private const int RenderBufferInfoSize = 0x18;
|
||||
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
|
||||
|
||||
private static readonly object StateGate = new();
|
||||
private static readonly Dictionary<ulong, SystemState> Systems = new();
|
||||
private static readonly Dictionary<ulong, RackState> Racks = new();
|
||||
private static readonly Dictionary<ulong, VoiceState> Voices = new();
|
||||
private static long _nextUid;
|
||||
private static long _renderCount;
|
||||
|
||||
private sealed record SystemState(uint Uid);
|
||||
private sealed record RackState(ulong SystemHandle, uint RackId);
|
||||
private sealed record VoiceState(ulong RackHandle, uint VoiceIndex);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "mPYgU4oYpuY",
|
||||
ExportName = "sceNgs2SystemCreateWithAllocator",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2SystemCreateWithAllocator(CpuContext ctx)
|
||||
{
|
||||
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
|
||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (StateGate)
|
||||
{
|
||||
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "u-WrYDaJA3k",
|
||||
ExportName = "sceNgs2SystemDestroy",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2SystemDestroy(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
lock (StateGate)
|
||||
{
|
||||
if (!Systems.Remove(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
|
||||
var rackHandles = Racks
|
||||
.Where(pair => pair.Value.SystemHandle == handle)
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray();
|
||||
foreach (var rackHandle in rackHandles)
|
||||
{
|
||||
RemoveRackLocked(rackHandle);
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "U546k6orxQo",
|
||||
ExportName = "sceNgs2RackCreateWithAllocator",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2RackCreateWithAllocator(CpuContext ctx)
|
||||
{
|
||||
var systemHandle = ctx[CpuRegister.Rdi];
|
||||
var rackId = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
var outHandleAddress = ctx[CpuRegister.R8];
|
||||
lock (StateGate)
|
||||
{
|
||||
if (!Systems.ContainsKey(systemHandle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
}
|
||||
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
|
||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (StateGate)
|
||||
{
|
||||
Racks[handle] = new RackState(systemHandle, rackId);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lCqD7oycmIM",
|
||||
ExportName = "sceNgs2RackDestroy",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2RackDestroy(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
lock (StateGate)
|
||||
{
|
||||
if (!Racks.ContainsKey(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
||||
}
|
||||
|
||||
RemoveRackLocked(handle);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "MwmHz8pAdAo",
|
||||
ExportName = "sceNgs2RackGetVoiceHandle",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2RackGetVoiceHandle(CpuContext ctx)
|
||||
{
|
||||
var rackHandle = ctx[CpuRegister.Rdi];
|
||||
var voiceIndex = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||
lock (StateGate)
|
||||
{
|
||||
if (!Racks.ContainsKey(rackHandle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
||||
}
|
||||
|
||||
var existing = Voices.FirstOrDefault(
|
||||
pair => pair.Value.RackHandle == rackHandle && pair.Value.VoiceIndex == voiceIndex);
|
||||
if (existing.Key != 0)
|
||||
{
|
||||
return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) ||
|
||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (StateGate)
|
||||
{
|
||||
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uu94irFOGpA",
|
||||
ExportName = "sceNgs2VoiceControl",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceControl(CpuContext ctx)
|
||||
{
|
||||
lock (StateGate)
|
||||
{
|
||||
return ctx.SetReturn(
|
||||
Voices.ContainsKey(ctx[CpuRegister.Rdi])
|
||||
? 0
|
||||
: OrbisNgs2ErrorInvalidVoiceHandle);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AbYvTOZ8Pts",
|
||||
ExportName = "sceNgs2VoiceRunCommands",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "i0VnXM-C9fc",
|
||||
ExportName = "sceNgs2SystemRender",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2SystemRender(CpuContext ctx)
|
||||
{
|
||||
var systemHandle = ctx[CpuRegister.Rdi];
|
||||
var bufferInfoAddress = ctx[CpuRegister.Rsi];
|
||||
var bufferInfoCount = unchecked((uint)ctx[CpuRegister.Rdx]);
|
||||
lock (StateGate)
|
||||
{
|
||||
if (!Systems.ContainsKey(systemHandle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
}
|
||||
|
||||
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
for (uint i = 0; i < bufferInfoCount; i++)
|
||||
{
|
||||
var entryAddress = bufferInfoAddress + (i * RenderBufferInfoSize);
|
||||
if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) ||
|
||||
!ctx.TryReadUInt64(entryAddress + 8, out var bufferSize))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (bufferAddress != 0 && bufferSize != 0)
|
||||
{
|
||||
if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var count = Interlocked.Increment(ref _renderCount);
|
||||
if (ShouldTrace() && (count <= 4 || count % 10_000 == 0))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
||||
{
|
||||
handle = 0;
|
||||
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
|
||||
data.Clear();
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(data[0..8], handle);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
|
||||
return ctx.Memory.TryWrite(handle, data);
|
||||
}
|
||||
|
||||
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
||||
{
|
||||
Span<byte> zeroes = stackalloc byte[4096];
|
||||
zeroes.Clear();
|
||||
for (ulong offset = 0; offset < length;)
|
||||
{
|
||||
var chunkSize = (int)Math.Min((ulong)zeroes.Length, length - offset);
|
||||
if (!ctx.Memory.TryWrite(address + offset, zeroes[..chunkSize]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
offset += unchecked((uint)chunkSize);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void RemoveRackLocked(ulong rackHandle)
|
||||
{
|
||||
Racks.Remove(rackHandle);
|
||||
foreach (var voiceHandle in Voices
|
||||
.Where(pair => pair.Value.RackHandle == rackHandle)
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray())
|
||||
{
|
||||
Voices.Remove(voiceHandle);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool ShouldTrace() =>
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
}
|
||||
@@ -0,0 +1,70 @@
|
||||
// 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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
|
||||
return ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
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,137 @@
|
||||
// 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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> stateBytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(stateBytes, 1);
|
||||
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(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 ctx.SetReturn(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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceNp($"set_np_title_id title='{ReadTitleId(titleId)}'");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
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,79 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.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 ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> parameters = stackalloc byte[16];
|
||||
return ctx.Memory.TryRead(parameterAddress, parameters)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[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 ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> context = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(context, 1);
|
||||
return ctx.Memory.TryWrite(contextAddress, context)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[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 (ctx.TryWriteInt32(ctx[CpuRegister.Rdi], handle, checkNil: true) ||
|
||||
ctx.TryWriteInt32(ctx[CpuRegister.Rsi], handle, checkNil: true))
|
||||
{
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "tpFJ8LIKvPw",
|
||||
ExportName = "sceNpUniversalDataSystemRegisterContext",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpUniversalDataSystem")]
|
||||
public static int NpUniversalDataSystemRegisterContext(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
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 ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
TraceNpWebApi2("init", httpContextId, poolSize);
|
||||
return ctx.SetReturn(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 ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
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,470 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
/// <summary>
|
||||
/// Snapshot of the DualSense state, already translated to ORBIS pad
|
||||
/// conventions (SCE_PAD_BUTTON bits; sticks 0..255 with 128 centered;
|
||||
/// triggers 0..255).
|
||||
/// </summary>
|
||||
internal readonly record struct DualSenseState(
|
||||
bool Connected,
|
||||
uint Buttons,
|
||||
byte LeftX,
|
||||
byte LeftY,
|
||||
byte RightX,
|
||||
byte RightY,
|
||||
byte L2,
|
||||
byte R2);
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DualSense controller over raw HID on a background thread.
|
||||
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
|
||||
/// activated by requesting feature report 0x05), with hot-plug retry.
|
||||
/// </summary>
|
||||
internal static class DualSenseReader
|
||||
{
|
||||
private const ushort SonyVendorId = 0x054C;
|
||||
private const ushort DualSenseProductId = 0x0CE6;
|
||||
private const ushort DualSenseEdgeProductId = 0x0DF2;
|
||||
|
||||
// SCE_PAD_BUTTON bit values.
|
||||
private const uint ButtonL3 = 0x0002;
|
||||
private const uint ButtonR3 = 0x0004;
|
||||
private const uint ButtonOptions = 0x0008;
|
||||
private const uint ButtonUp = 0x0010;
|
||||
private const uint ButtonRight = 0x0020;
|
||||
private const uint ButtonDown = 0x0040;
|
||||
private const uint ButtonLeft = 0x0080;
|
||||
private const uint ButtonL2 = 0x0100;
|
||||
private const uint ButtonR2 = 0x0200;
|
||||
private const uint ButtonL1 = 0x0400;
|
||||
private const uint ButtonR1 = 0x0800;
|
||||
private const uint ButtonTriangle = 0x1000;
|
||||
private const uint ButtonCircle = 0x2000;
|
||||
private const uint ButtonCross = 0x4000;
|
||||
private const uint ButtonSquare = 0x8000;
|
||||
private const uint ButtonTouchPad = 0x100000;
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static DualSenseState _state;
|
||||
private static bool _started;
|
||||
|
||||
// Output (rumble/lightbar) state, all guarded by Gate.
|
||||
private static string? _devicePath;
|
||||
private static bool _bluetooth;
|
||||
private static bool _outputReady;
|
||||
private static bool _lightbarSetupPending;
|
||||
private static byte _outputSequence;
|
||||
private static FileStream? _outputStream;
|
||||
private static byte _motorLeft;
|
||||
private static byte _motorRight;
|
||||
private static byte _lightbarRed;
|
||||
private static byte _lightbarGreen;
|
||||
private static byte _lightbarBlue = 64; // PS-style blue default
|
||||
private static byte _playerLeds = 0x04; // center LED = player 1
|
||||
|
||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||
internal static void EnsureStarted()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (_started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_started = true;
|
||||
var thread = new Thread(ReadLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "DualSenseReader",
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryGetState(out DualSenseState state)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
state = _state;
|
||||
}
|
||||
|
||||
return state.Connected;
|
||||
}
|
||||
|
||||
private static void SetState(in DualSenseState state)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sets rumble; large = left/strong motor, small = right/weak.</summary>
|
||||
internal static void SetRumble(byte largeMotor, byte smallMotor)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (_motorLeft == largeMotor && _motorRight == smallMotor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_motorLeft = largeMotor;
|
||||
_motorRight = smallMotor;
|
||||
SendOutputLocked();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetLightbar(byte red, byte green, byte blue)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (_lightbarRed == red && _lightbarGreen == green && _lightbarBlue == blue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lightbarRed = red;
|
||||
_lightbarGreen = green;
|
||||
_lightbarBlue = blue;
|
||||
SendOutputLocked();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetLightbar() => SetLightbar(0, 0, 64);
|
||||
|
||||
private static void OnDeviceIdentified(string path, bool bluetooth)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_devicePath = path;
|
||||
_bluetooth = bluetooth;
|
||||
_outputReady = true;
|
||||
_lightbarSetupPending = true;
|
||||
// Announce ourselves on the hardware: default lightbar + player 1 LED.
|
||||
SendOutputLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnDeviceLost()
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_devicePath = null;
|
||||
_outputReady = false;
|
||||
_motorLeft = 0;
|
||||
_motorRight = 0;
|
||||
_outputStream?.Dispose();
|
||||
_outputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SendOutputLocked()
|
||||
{
|
||||
if (!_outputReady || _devicePath is null)
|
||||
{
|
||||
return; // flushed by OnDeviceIdentified once connected
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_outputStream is null)
|
||||
{
|
||||
var handle = HidNative.CreateFile(
|
||||
_devicePath,
|
||||
HidNative.GenericRead | HidNative.GenericWrite,
|
||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
||||
0, HidNative.OpenExisting, 0, 0);
|
||||
if (handle.IsInvalid)
|
||||
{
|
||||
handle.Dispose();
|
||||
return; // read-only device access: outputs unavailable
|
||||
}
|
||||
|
||||
_outputStream = new FileStream(handle, FileAccess.Write, bufferSize: 1);
|
||||
}
|
||||
|
||||
var report = BuildOutputReportLocked();
|
||||
_outputStream.Write(report, 0, report.Length);
|
||||
_outputStream.Flush();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_outputStream?.Dispose();
|
||||
_outputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildOutputReportLocked()
|
||||
{
|
||||
// Common 47-byte output payload (offsets per the DualSense output
|
||||
// report layout, same as Linux hid-playstation).
|
||||
Span<byte> common = stackalloc byte[47];
|
||||
common[0] = 0x03; // valid_flag0: compatible vibration + haptics select
|
||||
common[1] = 0x04 | 0x10; // valid_flag1: lightbar + player indicator
|
||||
common[2] = _motorRight; // right (weak) motor
|
||||
common[3] = _motorLeft; // left (strong) motor
|
||||
if (_lightbarSetupPending)
|
||||
{
|
||||
common[38] |= 0x02; // valid_flag2: lightbar setup control enable
|
||||
common[41] = 0x01; // lightbar_setup: light on
|
||||
_lightbarSetupPending = false;
|
||||
}
|
||||
|
||||
common[43] = _playerLeds;
|
||||
common[44] = _lightbarRed;
|
||||
common[45] = _lightbarGreen;
|
||||
common[46] = _lightbarBlue;
|
||||
|
||||
if (!_bluetooth)
|
||||
{
|
||||
var usbReport = new byte[48];
|
||||
usbReport[0] = 0x02;
|
||||
common.CopyTo(usbReport.AsSpan(1));
|
||||
return usbReport;
|
||||
}
|
||||
|
||||
// Bluetooth: 0x31 wrapper with sequence tag and CRC32 over a 0xA2
|
||||
// seed byte plus the first 74 report bytes.
|
||||
var btReport = new byte[78];
|
||||
btReport[0] = 0x31;
|
||||
btReport[1] = (byte)((_outputSequence & 0x0F) << 4);
|
||||
_outputSequence = (byte)((_outputSequence + 1) & 0x0F);
|
||||
btReport[2] = 0x10;
|
||||
common.CopyTo(btReport.AsSpan(3));
|
||||
var crc = Crc32(0xA2, btReport.AsSpan(0, 74));
|
||||
btReport[74] = (byte)crc;
|
||||
btReport[75] = (byte)(crc >> 8);
|
||||
btReport[76] = (byte)(crc >> 16);
|
||||
btReport[77] = (byte)(crc >> 24);
|
||||
return btReport;
|
||||
}
|
||||
|
||||
private static uint Crc32(byte seed, ReadOnlySpan<byte> data)
|
||||
{
|
||||
var crc = Crc32Update(0xFFFFFFFFu, seed);
|
||||
foreach (var value in data)
|
||||
{
|
||||
crc = Crc32Update(crc, value);
|
||||
}
|
||||
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
private static uint Crc32Update(uint crc, byte value)
|
||||
{
|
||||
crc ^= value;
|
||||
for (var bit = 0; bit < 8; bit++)
|
||||
{
|
||||
crc = (crc >> 1) ^ (0xEDB88320u & (uint)-(int)(crc & 1));
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
private static void ReadLoop()
|
||||
{
|
||||
var announcedConnect = false;
|
||||
while (true)
|
||||
{
|
||||
SafeFileHandle? handle = null;
|
||||
try
|
||||
{
|
||||
handle = OpenDualSense(out var devicePath);
|
||||
if (handle is null || devicePath is null)
|
||||
{
|
||||
SetState(default);
|
||||
announcedConnect = false;
|
||||
Thread.Sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bluetooth quirk: the DualSense sends a simplified report
|
||||
// until feature report 0x05 is requested, which switches it
|
||||
// to the full 0x31 input report. Harmless over USB.
|
||||
var feature = new byte[41];
|
||||
feature[0] = 0x05;
|
||||
_ = HidNative.HidD_GetFeature(handle, feature, feature.Length);
|
||||
|
||||
if (!announcedConnect)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] DualSense controller connected.");
|
||||
announcedConnect = true;
|
||||
}
|
||||
|
||||
using var stream = new FileStream(handle, FileAccess.Read, bufferSize: 1);
|
||||
handle = null; // stream owns it now
|
||||
var buffer = new byte[256];
|
||||
var transportKnown = false;
|
||||
while (true)
|
||||
{
|
||||
var read = stream.Read(buffer, 0, buffer.Length);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (TryParseReport(buffer.AsSpan(0, read), out var state))
|
||||
{
|
||||
if (!transportKnown)
|
||||
{
|
||||
// The first parsed report tells us the transport,
|
||||
// which the output (rumble/lightbar) path needs.
|
||||
transportKnown = true;
|
||||
OnDeviceIdentified(devicePath, bluetooth: buffer[0] == 0x31);
|
||||
}
|
||||
|
||||
SetState(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Unplugged or read error: fall through and retry.
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle?.Dispose();
|
||||
}
|
||||
|
||||
if (announcedConnect)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] DualSense controller disconnected.");
|
||||
announcedConnect = false;
|
||||
}
|
||||
|
||||
OnDeviceLost();
|
||||
SetState(default);
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
private static SafeFileHandle? OpenDualSense(out string? devicePath)
|
||||
{
|
||||
devicePath = null;
|
||||
foreach (var path in HidNative.EnumerateHidDevicePaths())
|
||||
{
|
||||
// Open without access rights just to query VID/PID.
|
||||
using var probe = HidNative.CreateFile(
|
||||
path, 0, HidNative.FileShareRead | HidNative.FileShareWrite, 0, HidNative.OpenExisting, 0, 0);
|
||||
if (probe.IsInvalid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var attributes = new HidNative.HiddAttributes { Size = 12 };
|
||||
if (!HidNative.HidD_GetAttributes(probe, ref attributes) ||
|
||||
attributes.VendorId != SonyVendorId ||
|
||||
(attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read+write so feature reports work; fall back to read-only.
|
||||
var handle = HidNative.CreateFile(
|
||||
path,
|
||||
HidNative.GenericRead | HidNative.GenericWrite,
|
||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
||||
0, HidNative.OpenExisting, 0, 0);
|
||||
if (handle.IsInvalid)
|
||||
{
|
||||
handle.Dispose();
|
||||
handle = HidNative.CreateFile(
|
||||
path,
|
||||
HidNative.GenericRead,
|
||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
||||
0, HidNative.OpenExisting, 0, 0);
|
||||
}
|
||||
|
||||
if (!handle.IsInvalid)
|
||||
{
|
||||
devicePath = path;
|
||||
return handle;
|
||||
}
|
||||
|
||||
handle.Dispose();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryParseReport(ReadOnlySpan<byte> report, out DualSenseState state)
|
||||
{
|
||||
// USB: report id 0x01, payload starts at [1].
|
||||
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
|
||||
int offset;
|
||||
if (report.Length >= 11 && report[0] == 0x01)
|
||||
{
|
||||
offset = 1;
|
||||
}
|
||||
else if (report.Length >= 12 && report[0] == 0x31)
|
||||
{
|
||||
offset = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
state = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var leftX = report[offset + 0];
|
||||
var leftY = report[offset + 1];
|
||||
var rightX = report[offset + 2];
|
||||
var rightY = report[offset + 3];
|
||||
var l2 = report[offset + 4];
|
||||
var r2 = report[offset + 5];
|
||||
var buttons0 = report[offset + 7];
|
||||
var buttons1 = report[offset + 8];
|
||||
var buttons2 = report[offset + 9];
|
||||
|
||||
uint buttons = 0;
|
||||
buttons |= (buttons0 & 0x10) != 0 ? ButtonSquare : 0;
|
||||
buttons |= (buttons0 & 0x20) != 0 ? ButtonCross : 0;
|
||||
buttons |= (buttons0 & 0x40) != 0 ? ButtonCircle : 0;
|
||||
buttons |= (buttons0 & 0x80) != 0 ? ButtonTriangle : 0;
|
||||
buttons |= HatToButtons(buttons0 & 0x0F);
|
||||
buttons |= (buttons1 & 0x01) != 0 ? ButtonL1 : 0;
|
||||
buttons |= (buttons1 & 0x02) != 0 ? ButtonR1 : 0;
|
||||
buttons |= (buttons1 & 0x04) != 0 ? ButtonL2 : 0;
|
||||
buttons |= (buttons1 & 0x08) != 0 ? ButtonR2 : 0;
|
||||
buttons |= (buttons1 & 0x20) != 0 ? ButtonOptions : 0;
|
||||
buttons |= (buttons1 & 0x40) != 0 ? ButtonL3 : 0;
|
||||
buttons |= (buttons1 & 0x80) != 0 ? ButtonR3 : 0;
|
||||
buttons |= (buttons2 & 0x02) != 0 ? ButtonTouchPad : 0;
|
||||
|
||||
state = new DualSenseState(
|
||||
Connected: true,
|
||||
Buttons: buttons,
|
||||
LeftX: leftX,
|
||||
LeftY: leftY,
|
||||
RightX: rightX,
|
||||
RightY: rightY,
|
||||
L2: l2,
|
||||
R2: r2);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static uint HatToButtons(int hat) => hat switch
|
||||
{
|
||||
0 => ButtonUp,
|
||||
1 => ButtonUp | ButtonRight,
|
||||
2 => ButtonRight,
|
||||
3 => ButtonRight | ButtonDown,
|
||||
4 => ButtonDown,
|
||||
5 => ButtonDown | ButtonLeft,
|
||||
6 => ButtonLeft,
|
||||
7 => ButtonLeft | ButtonUp,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,136 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Runtime.InteropServices;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal Win32 HID interop used to talk to a DualSense controller
|
||||
/// directly, without any external input library.
|
||||
/// </summary>
|
||||
internal static partial class HidNative
|
||||
{
|
||||
internal const int DigcfPresent = 0x02;
|
||||
internal const int DigcfDeviceInterface = 0x10;
|
||||
internal const uint GenericRead = 0x80000000;
|
||||
internal const uint GenericWrite = 0x40000000;
|
||||
internal const uint FileShareRead = 0x1;
|
||||
internal const uint FileShareWrite = 0x2;
|
||||
internal const uint OpenExisting = 3;
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct SpDeviceInterfaceData
|
||||
{
|
||||
public int CbSize;
|
||||
public Guid InterfaceClassGuid;
|
||||
public int Flags;
|
||||
public nint Reserved;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
internal struct HiddAttributes
|
||||
{
|
||||
public int Size;
|
||||
public ushort VendorId;
|
||||
public ushort ProductId;
|
||||
public ushort VersionNumber;
|
||||
}
|
||||
|
||||
[DllImport("hid.dll")]
|
||||
internal static extern void HidD_GetHidGuid(out Guid hidGuid);
|
||||
|
||||
[DllImport("hid.dll")]
|
||||
internal static extern bool HidD_GetAttributes(SafeFileHandle hidDeviceObject, ref HiddAttributes attributes);
|
||||
|
||||
[DllImport("hid.dll")]
|
||||
internal static extern bool HidD_GetFeature(SafeFileHandle hidDeviceObject, byte[] reportBuffer, int reportBufferLength);
|
||||
|
||||
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
|
||||
internal static extern nint SetupDiGetClassDevs(ref Guid classGuid, nint enumerator, nint hwndParent, int flags);
|
||||
|
||||
[DllImport("setupapi.dll")]
|
||||
internal static extern bool SetupDiEnumDeviceInterfaces(
|
||||
nint deviceInfoSet,
|
||||
nint deviceInfoData,
|
||||
ref Guid interfaceClassGuid,
|
||||
int memberIndex,
|
||||
ref SpDeviceInterfaceData deviceInterfaceData);
|
||||
|
||||
[DllImport("setupapi.dll", CharSet = CharSet.Unicode)]
|
||||
internal static extern bool SetupDiGetDeviceInterfaceDetail(
|
||||
nint deviceInfoSet,
|
||||
ref SpDeviceInterfaceData deviceInterfaceData,
|
||||
nint deviceInterfaceDetailData,
|
||||
int deviceInterfaceDetailDataSize,
|
||||
out int requiredSize,
|
||||
nint deviceInfoData);
|
||||
|
||||
[DllImport("setupapi.dll")]
|
||||
internal static extern bool SetupDiDestroyDeviceInfoList(nint deviceInfoSet);
|
||||
|
||||
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
|
||||
internal static extern SafeFileHandle CreateFile(
|
||||
string fileName,
|
||||
uint desiredAccess,
|
||||
uint shareMode,
|
||||
nint securityAttributes,
|
||||
uint creationDisposition,
|
||||
uint flagsAndAttributes,
|
||||
nint templateFile);
|
||||
|
||||
/// <summary>
|
||||
/// Enumerates the device paths of all present HID interfaces.
|
||||
/// </summary>
|
||||
internal static List<string> EnumerateHidDevicePaths()
|
||||
{
|
||||
var paths = new List<string>();
|
||||
HidD_GetHidGuid(out var hidGuid);
|
||||
var deviceInfoSet = SetupDiGetClassDevs(ref hidGuid, 0, 0, DigcfPresent | DigcfDeviceInterface);
|
||||
if (deviceInfoSet == -1 || deviceInfoSet == 0)
|
||||
{
|
||||
return paths;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var interfaceData = new SpDeviceInterfaceData
|
||||
{
|
||||
CbSize = Marshal.SizeOf<SpDeviceInterfaceData>(),
|
||||
};
|
||||
|
||||
for (var index = 0; SetupDiEnumDeviceInterfaces(deviceInfoSet, 0, ref hidGuid, index, ref interfaceData); index++)
|
||||
{
|
||||
SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref interfaceData, 0, 0, out var requiredSize, 0);
|
||||
if (requiredSize <= 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var detailBuffer = Marshal.AllocHGlobal(requiredSize);
|
||||
try
|
||||
{
|
||||
// SP_DEVICE_INTERFACE_DETAIL_DATA_W.cbSize is 8 on x64
|
||||
// (DWORD + aligned WCHAR[1]); the path string follows it.
|
||||
Marshal.WriteInt32(detailBuffer, 8);
|
||||
if (SetupDiGetDeviceInterfaceDetail(deviceInfoSet, ref interfaceData, detailBuffer, requiredSize, out _, 0) &&
|
||||
Marshal.PtrToStringUni(detailBuffer + 4) is { Length: > 0 } path)
|
||||
{
|
||||
paths.Add(path);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(detailBuffer);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
SetupDiDestroyDeviceInfoList(deviceInfoSet);
|
||||
}
|
||||
|
||||
return paths;
|
||||
}
|
||||
}
|
||||
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
@@ -29,7 +30,8 @@ public static class PadExports
|
||||
public static int PadInit(CpuContext ctx)
|
||||
{
|
||||
_initialized = true;
|
||||
return SetReturn(ctx, 0);
|
||||
DualSenseReader.EnsureStarted();
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -45,20 +47,24 @@ public static class PadExports
|
||||
var parameterAddress = ctx[CpuRegister.Rcx];
|
||||
if (!_initialized)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorNotInitialized);
|
||||
return ctx.SetReturn(OrbisPadErrorNotInitialized);
|
||||
}
|
||||
|
||||
if (userId == -1)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorDeviceNoHandle);
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
|
||||
}
|
||||
|
||||
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorDeviceNotConnected);
|
||||
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, PrimaryPadHandle);
|
||||
DualSenseReader.EnsureStarted();
|
||||
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
||||
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
||||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense will be used automatically when plugged in.");
|
||||
return ctx.SetReturn(PrimaryPadHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -70,8 +76,8 @@ public static class PadExports
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return handle == PrimaryPadHandle
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -85,12 +91,12 @@ public static class PadExports
|
||||
var informationAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (informationAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> information = stackalloc byte[ControllerInformationSize];
|
||||
@@ -105,8 +111,8 @@ public static class PadExports
|
||||
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
|
||||
|
||||
return ctx.Memory.TryWrite(informationAddress, information)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -120,20 +126,164 @@ public static class PadExports
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (dataAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
return WriteNeutralPadData(ctx, dataAddress)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((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 ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (dataAddress == 0 || count < 1 || count > 64)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
return WriteNeutralPadData(ctx, dataAddress)
|
||||
? ctx.SetReturn(1)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "W2G-yoyMF5U",
|
||||
ExportName = "scePadSetVibrationMode",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadSetVibrationMode(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yFVnOdGxvZY",
|
||||
ExportName = "scePadSetVibration",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadSetVibration(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (parameterAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// ScePadVibrationParam: { uint8_t largeMotor; uint8_t smallMotor; }
|
||||
Span<byte> parameter = stackalloc byte[2];
|
||||
if (!ctx.Memory.TryRead(parameterAddress, parameter))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
DualSenseReader.SetRumble(parameter[0], parameter[1]);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RR4novUEENY",
|
||||
ExportName = "scePadSetLightBar",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadSetLightBar(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var parameterAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (parameterAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
// ScePadColor: { uint8_t r; uint8_t g; uint8_t b; uint8_t reserved; }
|
||||
Span<byte> color = stackalloc byte[4];
|
||||
if (!ctx.Memory.TryRead(parameterAddress, color))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
DualSenseReader.SetLightbar(color[0], color[1], color[2]);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "DscD1i9HX1w",
|
||||
ExportName = "scePadResetLightBar",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libScePad")]
|
||||
public static int PadResetLightBar(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
DualSenseReader.ResetLightbar();
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
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;
|
||||
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
||||
var leftX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x41), IsKeyDown(0x44)) : (byte)128;
|
||||
var leftY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x57), IsKeyDown(0x53)) : (byte)128;
|
||||
var rightX = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x4A), IsKeyDown(0x4C)) : (byte)128;
|
||||
var rightY = acceptsKeyboardInput ? ReadAnalogStick(IsKeyDown(0x49), IsKeyDown(0x4B)) : (byte)128;
|
||||
var l2 = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
||||
var r2 = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
||||
|
||||
if (DualSenseReader.TryGetState(out var pad))
|
||||
{
|
||||
buttons |= pad.Buttons;
|
||||
// The controller stick wins whenever it is deflected past a
|
||||
// small deadzone; otherwise any keyboard value stays.
|
||||
leftX = MergeAxis(pad.LeftX, leftX);
|
||||
leftY = MergeAxis(pad.LeftY, leftY);
|
||||
rightX = MergeAxis(pad.RightX, rightX);
|
||||
rightY = MergeAxis(pad.RightY, rightY);
|
||||
l2 = Math.Max(l2, pad.L2);
|
||||
r2 = Math.Max(r2, pad.R2);
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
||||
data[0x04] = leftX;
|
||||
data[0x05] = leftY;
|
||||
data[0x06] = rightX;
|
||||
data[0x07] = rightY;
|
||||
data[0x08] = l2;
|
||||
data[0x09] = r2;
|
||||
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
|
||||
data[0x4C] = 1;
|
||||
var timestampTicks = Stopwatch.GetTimestamp();
|
||||
@@ -145,14 +295,66 @@ public static class PadExports
|
||||
timestampMicroseconds);
|
||||
data[0x68] = 1;
|
||||
|
||||
return ctx.Memory.TryWrite(dataAddress, data)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.Memory.TryWrite(dataAddress, data);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
[DllImport("user32.dll")]
|
||||
private static extern short GetAsyncKeyState(int vKey);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern nint GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint processId);
|
||||
|
||||
private static bool IsKeyDown(int vk) =>
|
||||
(GetAsyncKeyState(vk) & 0x8000) != 0;
|
||||
|
||||
private static bool IsEmulatorWindowFocused()
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
var foregroundWindow = GetForegroundWindow();
|
||||
if (foregroundWindow == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
GetWindowThreadProcessId(foregroundWindow, out var processId);
|
||||
return processId == (uint)Environment.ProcessId;
|
||||
}
|
||||
|
||||
private static uint ReadKeyboardButtons()
|
||||
{
|
||||
uint buttons = 0;
|
||||
// D-pad
|
||||
if (IsKeyDown(0x25)) buttons |= 0x0080; // Left
|
||||
if (IsKeyDown(0x27)) buttons |= 0x0020; // Right
|
||||
if (IsKeyDown(0x26)) buttons |= 0x0010; // Up
|
||||
if (IsKeyDown(0x28)) buttons |= 0x0040; // Down
|
||||
// Face buttons
|
||||
if (IsKeyDown(0x5A) || IsKeyDown(0x0D)) buttons |= 0x4000; // Z / Enter = Cross
|
||||
if (IsKeyDown(0x58) || IsKeyDown(0x1B)) buttons |= 0x2000; // X / Escape = Circle
|
||||
if (IsKeyDown(0x43)) buttons |= 0x8000; // C = Square
|
||||
if (IsKeyDown(0x56)) buttons |= 0x1000; // V = Triangle
|
||||
// Shoulder buttons
|
||||
if (IsKeyDown(0x51)) buttons |= 0x0400; // Q = L1
|
||||
if (IsKeyDown(0x45)) buttons |= 0x0800; // E = R1
|
||||
if (IsKeyDown(0x52)) buttons |= 0x0100; // R = L2 (digital)
|
||||
if (IsKeyDown(0x46)) buttons |= 0x0200; // F = R2 (digital)
|
||||
// Options (Start)
|
||||
if (IsKeyDown(0x09) || IsKeyDown(0x08)) buttons |= 0x0008; // Tab / Backspace = Options
|
||||
return buttons;
|
||||
}
|
||||
|
||||
private static byte ReadAnalogStick(bool negative, bool positive)
|
||||
{
|
||||
if (negative && !positive) return 0;
|
||||
if (positive && !negative) return 255;
|
||||
return 128;
|
||||
}
|
||||
|
||||
private static byte MergeAxis(byte controller, byte keyboard)
|
||||
{
|
||||
const int Deadzone = 10;
|
||||
return Math.Abs(controller - 128) > Deadzone ? controller : keyboard;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,570 @@ 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 ctx.TryWriteUInt32(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 (!ctx.TryWriteUInt16(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 ctx.TryWriteUInt32(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
|
||||
: ctx.TryWriteInt64(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 ctx.TryWriteInt32(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 (!ctx.TryReadUInt16(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 ctx.TryWriteUInt32(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 (!ctx.TryReadUInt16(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 false;
|
||||
return PlayGoMetadata.Empty;
|
||||
}
|
||||
|
||||
var hostPath = Path.Combine(app0Root, "sce_sys", "playgo-chunk.dat");
|
||||
return File.Exists(hostPath);
|
||||
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 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,193 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
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 ctx.SetReturn(ErrorAlreadyInitialized);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(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 ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (_status is not (StatusInitialized or StatusFinished))
|
||||
{
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
|
||||
_lastUserData = ctx.TryReadUInt64(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 ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ERKzksauAJA",
|
||||
ExportName = "sceSaveDataDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogGetStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KK3Bdg1RWK0",
|
||||
ExportName = "sceSaveDataDialogUpdateStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "en7gNVnh878",
|
||||
ExportName = "sceSaveDataDialogIsReadyToDisplay",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => ctx.SetReturn(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 ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) != StatusFinished)
|
||||
{
|
||||
return ctx.SetReturn(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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(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 ctx.SetReturn(ErrorNotRunning);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(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 ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
_lastMode = 0;
|
||||
_lastUserData = 0;
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "V-uEeFKARJU",
|
||||
ExportName = "sceSaveDataDialogProgressBarInc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hay1CfTmLyA",
|
||||
ExportName = "sceSaveDataDialogProgressBarSetValue",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => ctx.SetReturn(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 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,530 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
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 OrbisSaveDataErrorExists = unchecked((int)0x809F0007);
|
||||
private const int OrbisSaveDataErrorNotFound = unchecked((int)0x809F0008);
|
||||
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 const uint MountModeCreate = 1u << 2;
|
||||
private const uint MountModeCreate2 = 1u << 5;
|
||||
private const int MountResultSize = 0x40;
|
||||
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 ctx.SetReturn(0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return ctx.SetReturn(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 ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
|
||||
!TryReadSearchResult(ctx, resultAddress, out var result))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
string titleId;
|
||||
if (cond.TitleIdAddress == 0)
|
||||
{
|
||||
titleId = ResolveConfiguredTitleId();
|
||||
}
|
||||
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
|
||||
{
|
||||
return ctx.SetReturn((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 (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
||||
!ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
||||
{
|
||||
return ctx.SetReturn((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 ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
if (result.DirNamesAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(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 ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ZP4e7rlzOUk",
|
||||
ExportName = "sceSaveDataMount3",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataMount3(CpuContext ctx)
|
||||
{
|
||||
var mountAddress = ctx[CpuRegister.Rdi];
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
if (mountAddress == 0 || resultAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadInt32(mountAddress, out var userId) ||
|
||||
!ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) ||
|
||||
!ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) ||
|
||||
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
|
||||
!ctx.TryReadUInt32(mountAddress + 0x20, out var mountMode) ||
|
||||
!ctx.TryReadUInt32(mountAddress + 0x24, out var resource) ||
|
||||
!ctx.TryReadUInt32(mountAddress + 0x28, out var mode) ||
|
||||
dirNameAddress == 0 ||
|
||||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var titleId = ResolveConfiguredTitleId();
|
||||
var savePath = Path.Combine(
|
||||
ResolveTitleSaveRoot(userId, titleId),
|
||||
SanitizePathSegment(dirName));
|
||||
var existed = Directory.Exists(savePath);
|
||||
var create = (mountMode & MountModeCreate) != 0;
|
||||
var createIfMissing = (mountMode & MountModeCreate2) != 0;
|
||||
|
||||
if (!existed && !create && !createIfMissing)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorNotFound);
|
||||
}
|
||||
|
||||
if (existed && create)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorExists);
|
||||
}
|
||||
|
||||
if (!existed)
|
||||
{
|
||||
Directory.CreateDirectory(savePath);
|
||||
}
|
||||
|
||||
const string mountPoint = "/savedata0";
|
||||
KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, savePath);
|
||||
|
||||
Span<byte> result = stackalloc byte[MountResultSize];
|
||||
result.Clear();
|
||||
WriteAscii(result[..16], mountPoint);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u);
|
||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceSaveData(
|
||||
$"mount3 user={userId} title={titleId} dir={dirName} blocks={blocks} " +
|
||||
$"system_blocks={systemBlocks} mount_mode=0x{mountMode:X} resource={resource} mode={mode} " +
|
||||
$"mount_point={mountPoint} created={!existed} root='{savePath}'");
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
}
|
||||
|
||||
private static int _nextTransactionResource;
|
||||
[SysAbiExport(
|
||||
Nid = "gjRZNnw0JPE",
|
||||
ExportName = "sceSaveDataCreateTransactionResource",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataCreateTransactionResource(CpuContext ctx)
|
||||
{
|
||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var reserved = ctx[CpuRegister.Rsi];
|
||||
var resourceAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (resourceAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
||||
|
||||
if (!ctx.TryWriteUInt32(resourceAddress, id))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceSaveData(
|
||||
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
||||
{
|
||||
cond = default;
|
||||
if (!ctx.TryReadInt32(address, out var userId) ||
|
||||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
||||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
||||
!ctx.TryReadUInt32(address + 0x18, out var sortKey) ||
|
||||
!ctx.TryReadUInt32(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) ||
|
||||
!ctx.TryReadUInt32(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(AppContext.BaseDirectory, "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 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,105 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Text;
|
||||
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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
|
||||
TraceShare($"initialize memory=0x{memorySize:X} priority={priority} affinity=0x{affinityMask:X}");
|
||||
return ctx.SetReturn(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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedUtf8(ctx, contentParamAddress, MaxContentParamBytes, out var contentParam))
|
||||
{
|
||||
return ctx.SetReturn(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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
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,17 @@ 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.EXT" />
|
||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
||||
<PackageReference Include="Silk.NET.Windowing" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user