mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
44 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9ddc09ea91 | |||
| c4326a1143 | |||
| 347e33f3c9 | |||
| 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 |
+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: 120 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB |
@@ -126,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,16 +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 |
|
Before Width: | Height: | Size: 1.1 MiB 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,108 @@
|
||||
"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",
|
||||
@@ -59,6 +158,38 @@
|
||||
"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",
|
||||
@@ -73,6 +204,16 @@
|
||||
"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"
|
||||
},
|
||||
@@ -81,6 +222,7 @@
|
||||
"dependencies": {
|
||||
"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, )"
|
||||
}
|
||||
@@ -88,6 +230,48 @@
|
||||
"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, )",
|
||||
@@ -103,6 +287,16 @@
|
||||
"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, )",
|
||||
@@ -122,9 +316,61 @@
|
||||
"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": {
|
||||
"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",
|
||||
@@ -132,6 +378,52 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -139,6 +431,52 @@
|
||||
}
|
||||
},
|
||||
"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",
|
||||
@@ -146,4 +484,4 @@
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
@@ -80,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
|
||||
{
|
||||
@@ -89,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;
|
||||
}
|
||||
}
|
||||
@@ -103,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
|
||||
{
|
||||
@@ -119,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;
|
||||
}
|
||||
}
|
||||
@@ -134,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;
|
||||
@@ -306,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);
|
||||
@@ -415,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))
|
||||
@@ -456,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,
|
||||
|
||||
@@ -338,10 +338,15 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
orbisGen2Result = DispatchBootstrapBridge();
|
||||
}
|
||||
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal))
|
||||
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
|
||||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
|
||||
{
|
||||
orbisGen2Result = DispatchKernelDynlibDlsym();
|
||||
}
|
||||
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
|
||||
{
|
||||
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
|
||||
}
|
||||
else if (importStubEntry.Export is { } cachedExport &&
|
||||
(cachedExport.Target & cpuContext.TargetGeneration) != 0)
|
||||
{
|
||||
@@ -353,12 +358,21 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
orbisGen2Result = (OrbisGen2Result)returnValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchResolved = false;
|
||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||
}
|
||||
else if (importStubEntry.Export is { } mismatchedExport)
|
||||
{
|
||||
dispatchResolved = true;
|
||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Import#{num} not implemented for generation {cpuContext.TargetGeneration}: " +
|
||||
$"nid={importStubEntry.Nid} targets={mismatchedExport.Target} ret=0x{num7:X16}");
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchResolved = false;
|
||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -628,25 +642,56 @@ public sealed partial class DirectExecutionBackend
|
||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||
"qvMUCyyaCSI"; // sceKernelAprSubmitCommandBufferAndGetId
|
||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||
"Q2V+iqvjgC0" or // vsnprintf
|
||||
"q1cHNfGycLI" or // scePadRead
|
||||
"xk0AcarP3V4" or // scePadOpen
|
||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
||||
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
|
||||
"K-jXhbt2gn4"; // scePthreadMutexTrylock
|
||||
|
||||
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
||||
{
|
||||
var resultValue = unchecked((int)result);
|
||||
if (resultValue > 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var expectedFileProbeMiss =
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND &&
|
||||
IsExpectedFileProbeNotFoundNid(nid);
|
||||
var expectedTimedWaitTimeout =
|
||||
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
|
||||
unchecked((int)result) == 60;
|
||||
var expectedEqueueTimeout =
|
||||
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
var expectedMutexTrylockBusy =
|
||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||
if (!expectedFileProbeMiss && !expectedTimedWaitTimeout && !expectedMutexTrylockBusy)
|
||||
var expectedUserServiceNoEvent =
|
||||
string.Equals(nid, "yH17Q6NWtVg", StringComparison.Ordinal) &&
|
||||
resultValue == unchecked((int)0x80960007);
|
||||
var expectedPrivacyInvalidParameter =
|
||||
string.Equals(nid, "D-CzAxQL0XI", StringComparison.Ordinal) &&
|
||||
resultValue == unchecked((int)0x80960009);
|
||||
if (!expectedFileProbeMiss &&
|
||||
!expectedTimedWaitTimeout &&
|
||||
!expectedEqueueTimeout &&
|
||||
!expectedMutexTrylockBusy &&
|
||||
!expectedUserServiceNoEvent &&
|
||||
!expectedPrivacyInvalidParameter)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var key = nid + "\0" + (int)result;
|
||||
if (!ShouldLogExpectedImportResults())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var key = nid + "\0" + resultValue;
|
||||
int count;
|
||||
lock (_importResultLogSampleGate)
|
||||
{
|
||||
@@ -658,6 +703,12 @@ public sealed partial class DirectExecutionBackend
|
||||
return count <= 8 || count % 10000 == 0;
|
||||
}
|
||||
|
||||
private static bool ShouldLogExpectedImportResults() =>
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EXPECTED_IMPORT_RESULTS"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private static bool IsExpectedFileProbeNotFoundNid(string nid) =>
|
||||
nid is
|
||||
"eV9wAD2riIA" or // sceKernelStat
|
||||
@@ -723,6 +774,7 @@ public sealed partial class DirectExecutionBackend
|
||||
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
|
||||
"TywrFKCoLGY" or // sceSaveDataInitialize3
|
||||
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
|
||||
"ZP4e7rlzOUk" or // sceSaveDataMount3
|
||||
"ERKzksauAJA" or // sceSaveDataDialogGetStatus
|
||||
"KK3Bdg1RWK0" or // sceSaveDataDialogUpdateStatus
|
||||
"en7gNVnh878" or // sceSaveDataDialogIsReadyToDisplay
|
||||
@@ -1179,8 +1231,11 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress))
|
||||
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
|
||||
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'");
|
||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -1193,6 +1248,62 @@ public sealed partial class DirectExecutionBackend
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
var alias = symbolName switch
|
||||
{
|
||||
"scriptingGetMem" => "malloc",
|
||||
"scriptingFreeMem" => "free",
|
||||
"scriptingRealloc" => "realloc",
|
||||
"scriptingCalloc" => "calloc",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
return alias != null && TryResolveRuntimeSymbolAddress(alias, out address);
|
||||
}
|
||||
|
||||
private OrbisGen2Result DispatchIl2CppApiLookupSymbol()
|
||||
{
|
||||
var cpuContext = ActiveCpuContext;
|
||||
if (cpuContext == null)
|
||||
{
|
||||
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var symbolNameAddress = cpuContext[CpuRegister.Rdi];
|
||||
var outputAddress = cpuContext[CpuRegister.Rsi];
|
||||
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
|
||||
outputAddress == 0 ||
|
||||
!TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress) ||
|
||||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}' out=0x{outputAddress:X16}");
|
||||
if (outputAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt64Compat(outputAddress, 0);
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = 0;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address)
|
||||
{
|
||||
if (TryResolveRuntimeSymbolAddress(symbolName, out address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) &&
|
||||
TryResolveRuntimeSymbolAddress(symbol.Nid, out address);
|
||||
}
|
||||
|
||||
private OrbisGen2Result DispatchBootstrapBridge()
|
||||
{
|
||||
var cpuContext = ActiveCpuContext;
|
||||
|
||||
@@ -355,6 +355,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
public GuestThreadRunState State { get; set; }
|
||||
|
||||
public ulong ExitValue { get; set; }
|
||||
|
||||
public string? BlockReason { get; set; }
|
||||
|
||||
public bool HasBlockedContinuation { get; set; }
|
||||
@@ -1266,7 +1268,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
libraryName.IndexOf("Kernel", StringComparison.OrdinalIgnoreCase) >= 0;
|
||||
}
|
||||
|
||||
private static bool PreferLleForLibcExport(string exportName)
|
||||
private bool PreferLleForLibcExport(string exportName)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(exportName))
|
||||
{
|
||||
@@ -1287,6 +1289,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
return true;
|
||||
}
|
||||
if (IsLibcAllocatorExport(exportName))
|
||||
{
|
||||
return CanUseLleLibcAllocatorFamily();
|
||||
}
|
||||
if (string.Equals(value, "0", StringComparison.Ordinal))
|
||||
{
|
||||
return true;
|
||||
@@ -1298,6 +1304,51 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return IsSafeLleLibcExport(exportName);
|
||||
}
|
||||
|
||||
private bool CanUseLleLibcAllocatorFamily()
|
||||
{
|
||||
return HasUsableLleLibcExport("gQX+4GDQjpM", "malloc") &&
|
||||
HasUsableLleLibcExport("tIhsqj0qsFE", "free") &&
|
||||
HasUsableLleLibcExport("2X5agFjKxMc", "calloc") &&
|
||||
HasUsableLleLibcExport("Y7aJ1uydPMo", "realloc") &&
|
||||
HasUsableLleLibcExport("Ujf3KzMvRmI", "memalign") &&
|
||||
HasUsableLleLibcExport("2Btkg8k24Zg", "aligned_alloc") &&
|
||||
HasUsableLleLibcExport("cVSk9y8URbc", "posix_memalign");
|
||||
}
|
||||
|
||||
private bool HasUsableLleLibcExport(string nid, string exportName)
|
||||
{
|
||||
if (TryResolveRuntimeSymbolAddress(nid, out var address) && IsDirectImportTargetUsable(address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
foreach (var candidate in EnumerateRuntimeSymbolCandidates(exportName))
|
||||
{
|
||||
if (TryResolveRuntimeSymbolAddress(candidate, out address) && IsDirectImportTargetUsable(address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
private static bool IsLibcAllocatorExport(string exportName)
|
||||
{
|
||||
return exportName switch
|
||||
{
|
||||
"malloc" or
|
||||
"free" or
|
||||
"calloc" or
|
||||
"realloc" or
|
||||
"memalign" or
|
||||
"aligned_alloc" or
|
||||
"posix_memalign" or
|
||||
"malloc_usable_size" => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
|
||||
private static bool IsSafeLleLibcExport(string exportName)
|
||||
{
|
||||
return exportName switch
|
||||
@@ -1628,10 +1679,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ptr2[num++] = 65;
|
||||
ptr2[num++] = 95;
|
||||
ptr2[num++] = 195;
|
||||
uint num2 = default(uint);
|
||||
VirtualProtect(ptr, 192u, 32u, &num2);
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
|
||||
return (nint)ptr;
|
||||
uint num2 = default(uint);
|
||||
if (!VirtualProtect(ptr, 192u, 32u, &num2))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
|
||||
return (nint)ptr;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -1712,7 +1767,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
tlsHandlerAddress[num++] = 195;
|
||||
_tlsPatchStubOffset = (num + 15) & ~15;
|
||||
uint num2 = default(uint);
|
||||
VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2);
|
||||
if (!VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS handler at 0x{_tlsHandlerAddress:X16}");
|
||||
return;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, TlsHandlerRegionSize);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] TLS handler at 0x{_tlsHandlerAddress:X16}");
|
||||
}
|
||||
@@ -1733,7 +1792,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ptr2[i] = 144;
|
||||
}
|
||||
uint num = default(uint);
|
||||
VirtualProtect(ptr, 4096u, 32u, &num);
|
||||
if (!VirtualProtect(ptr, 4096u, 32u, &num))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for unresolved return stub at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, 16u);
|
||||
return (nint)ptr;
|
||||
}
|
||||
@@ -1780,7 +1843,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
uint oldProtect = default;
|
||||
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for guest return stub at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||
return (nint)ptr;
|
||||
}
|
||||
@@ -1872,7 +1939,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
|
||||
|
||||
uint oldProtect = default;
|
||||
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||
return (nint)ptr;
|
||||
}
|
||||
@@ -2203,7 +2274,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ptr[num2++] = 144;
|
||||
}
|
||||
uint flNewProtect = default(uint);
|
||||
VirtualProtect((void*)num, 32u, 32u, &flNewProtect);
|
||||
if (!VirtualProtect((void*)num, 32u, 32u, &flNewProtect))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS store helper at 0x{num:X16}");
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)num, 32u);
|
||||
return num;
|
||||
}
|
||||
@@ -2406,6 +2481,69 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
public bool SupportsGuestContextTransfer => true;
|
||||
|
||||
public bool TryJoinThread(
|
||||
CpuContext callerContext,
|
||||
ulong threadHandle,
|
||||
out ulong returnValue,
|
||||
out string? error)
|
||||
{
|
||||
returnValue = 0;
|
||||
error = null;
|
||||
if (threadHandle == 0)
|
||||
{
|
||||
error = "thread handle is zero";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (threadHandle == GuestThreadExecution.CurrentGuestThreadHandle)
|
||||
{
|
||||
error = "thread cannot join itself";
|
||||
return false;
|
||||
}
|
||||
|
||||
while (!ActiveForcedGuestExit)
|
||||
{
|
||||
Thread? hostThread;
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
if (!_guestThreads.TryGetValue(threadHandle, out var thread))
|
||||
{
|
||||
error = $"unknown guest thread 0x{threadHandle:X16}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (thread.State == GuestThreadRunState.Exited)
|
||||
{
|
||||
returnValue = thread.ExitValue;
|
||||
return true;
|
||||
}
|
||||
|
||||
if (thread.State == GuestThreadRunState.Faulted)
|
||||
{
|
||||
error =
|
||||
$"guest thread 0x{threadHandle:X16} faulted: " +
|
||||
(thread.BlockReason ?? "unknown error");
|
||||
return false;
|
||||
}
|
||||
|
||||
hostThread = thread.HostThread;
|
||||
}
|
||||
|
||||
if (hostThread is not null &&
|
||||
!ReferenceEquals(hostThread, Thread.CurrentThread))
|
||||
{
|
||||
hostThread.Join(1);
|
||||
}
|
||||
else
|
||||
{
|
||||
Thread.Sleep(1);
|
||||
}
|
||||
}
|
||||
|
||||
error = "guest execution stopped while joining thread";
|
||||
return false;
|
||||
}
|
||||
|
||||
public void Pump(CpuContext callerContext, string reason)
|
||||
{
|
||||
_ = callerContext;
|
||||
@@ -3254,6 +3392,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
switch (exitReason)
|
||||
{
|
||||
case GuestNativeCallExitReason.Returned:
|
||||
thread.ExitValue = thread.Context[CpuRegister.Rax];
|
||||
thread.State = GuestThreadRunState.Exited;
|
||||
break;
|
||||
case GuestNativeCallExitReason.Blocked:
|
||||
@@ -3476,7 +3615,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||
{
|
||||
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
@@ -3613,7 +3756,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||
{
|
||||
reason = $"VirtualProtect failed for guest continuation stub at 0x{(nint)ptr:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
@@ -3873,7 +4020,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return false;
|
||||
}
|
||||
uint num5 = default(uint);
|
||||
VirtualProtect(ptr, stubSize, 64u, &num5);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &num5))
|
||||
{
|
||||
LastError = $"VirtualProtect failed for guest entry stub at 0x{(nint)ptr:X16}";
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return false;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
if (_hostRspSlotStorage != 0)
|
||||
{
|
||||
|
||||
@@ -4,14 +4,19 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Memory;
|
||||
|
||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
||||
{
|
||||
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;
|
||||
@@ -19,7 +24,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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;
|
||||
@@ -35,6 +41,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
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);
|
||||
@@ -51,7 +58,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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)
|
||||
{
|
||||
@@ -81,7 +92,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
@@ -96,7 +107,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -110,7 +121,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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)
|
||||
@@ -139,7 +152,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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)
|
||||
@@ -193,12 +206,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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
|
||||
@@ -210,7 +223,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
@@ -227,7 +240,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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;
|
||||
}
|
||||
@@ -247,7 +260,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
var alignedSize = AlignUp(size, PageSize);
|
||||
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
|
||||
var cursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||
|
||||
for (var attempt = 0; attempt < 0x10000; attempt++)
|
||||
{
|
||||
@@ -267,6 +281,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
||||
if (actualAddress == cursor)
|
||||
{
|
||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -334,6 +349,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
_regions.Clear();
|
||||
_pageProtections.Clear();
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
_allocationSearchHints.Clear();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -390,7 +409,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
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
|
||||
{
|
||||
@@ -439,7 +458,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||
{
|
||||
FlushInstructionCache(null, (void*)address, (nuint)size);
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,30 +492,34 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_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 (region.IsReservedOnly)
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||
@@ -533,30 +556,34 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_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 (region.IsReservedOnly)
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (byte* srcPtr = source)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
@@ -589,13 +616,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
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))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
@@ -637,13 +665,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
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))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
@@ -677,7 +706,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||
if (IsExecutableProtection(oldProtect))
|
||||
{
|
||||
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
||||
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,15 +728,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_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
|
||||
{
|
||||
@@ -720,14 +743,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_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
|
||||
{
|
||||
@@ -737,14 +753,48 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
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)
|
||||
@@ -762,6 +812,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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);
|
||||
@@ -776,6 +836,37 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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)
|
||||
{
|
||||
offset = 0;
|
||||
@@ -944,6 +1035,29 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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)
|
||||
|
||||
@@ -11,6 +11,7 @@ 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;
|
||||
@@ -20,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)
|
||||
@@ -128,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;
|
||||
@@ -140,7 +143,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
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);
|
||||
@@ -163,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);
|
||||
@@ -171,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,
|
||||
@@ -182,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);
|
||||
@@ -434,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,
|
||||
@@ -446,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;
|
||||
}
|
||||
}
|
||||
@@ -468,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",
|
||||
@@ -508,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,
|
||||
@@ -543,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)
|
||||
@@ -554,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);
|
||||
@@ -562,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
|
||||
@@ -575,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)
|
||||
@@ -583,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;
|
||||
@@ -619,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;
|
||||
}
|
||||
|
||||
@@ -650,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}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,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++;
|
||||
@@ -696,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++;
|
||||
@@ -706,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++;
|
||||
@@ -743,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;
|
||||
@@ -853,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>
|
||||
|
||||
@@ -72,6 +72,7 @@
|
||||
"dependencies": {
|
||||
"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, )"
|
||||
}
|
||||
@@ -88,6 +89,16 @@
|
||||
"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, )",
|
||||
|
||||
@@ -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,265 @@
|
||||
<!--
|
||||
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/SharpEmu.ico" Width="20" Height="20"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||
<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,35 @@
|
||||
<!--
|
||||
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\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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,12 @@ public interface IGuestThreadScheduler
|
||||
|
||||
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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1011
-429
File diff suppressed because it is too large
Load Diff
@@ -60,7 +60,8 @@ internal sealed record Gen5SpirvShader(
|
||||
byte[] Spirv,
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
||||
uint AttributeCount);
|
||||
uint AttributeCount,
|
||||
IReadOnlyList<Gen5VertexInputBinding> VertexInputs);
|
||||
|
||||
internal readonly record struct Gen5ShaderResourceMapping(
|
||||
Gen5ShaderResourceKind Kind,
|
||||
@@ -271,6 +272,17 @@ internal sealed record Gen5GlobalMemoryBinding(
|
||||
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,
|
||||
@@ -278,7 +290,8 @@ internal sealed record Gen5ShaderEvaluation(
|
||||
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
|
||||
IReadOnlySet<uint>? RuntimeScalarRegisters = null);
|
||||
IReadOnlySet<uint>? RuntimeScalarRegisters = null,
|
||||
IReadOnlyList<Gen5VertexInputBinding>? VertexInputs = null);
|
||||
|
||||
internal sealed record Gen5ShaderInstruction(
|
||||
uint Pc,
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
@@ -18,9 +17,9 @@ internal static class Gen5ShaderMetadataReader
|
||||
out Gen5ShaderMetadata metadata)
|
||||
{
|
||||
metadata = default!;
|
||||
if (!TryReadUInt64(ctx, shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
||||
if (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
||||
userDataAddress == 0 ||
|
||||
!TryReadUInt64(ctx, userDataAddress, out var directResourceOffsetsAddress))
|
||||
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -28,8 +27,7 @@ internal static class Gen5ShaderMetadataReader
|
||||
var resourceOffsets = new ulong[ResourceClassCount];
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
if (!TryReadUInt64(
|
||||
ctx,
|
||||
if (!ctx.TryReadUInt64(
|
||||
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
|
||||
out resourceOffsets[resourceClass]))
|
||||
{
|
||||
@@ -37,9 +35,9 @@ internal static class Gen5ShaderMetadataReader
|
||||
}
|
||||
}
|
||||
|
||||
if (!TryReadUInt16(ctx, userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||
!TryReadUInt16(ctx, userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||
!TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount) ||
|
||||
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;
|
||||
@@ -48,8 +46,7 @@ internal static class Gen5ShaderMetadataReader
|
||||
var resourceCounts = new ushort[ResourceClassCount];
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
if (!TryReadUInt16(
|
||||
ctx,
|
||||
if (!ctx.TryReadUInt16(
|
||||
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
|
||||
out resourceCounts[resourceClass]) ||
|
||||
resourceCounts[resourceClass] > MaxMetadataEntries)
|
||||
@@ -68,7 +65,7 @@ internal static class Gen5ShaderMetadataReader
|
||||
|
||||
for (uint type = 0; type < directResourceCount; type++)
|
||||
{
|
||||
if (!TryReadUInt16(ctx, directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
|
||||
if (!ctx.TryReadUInt16(directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -96,8 +93,7 @@ internal static class Gen5ShaderMetadataReader
|
||||
|
||||
for (uint slot = 0; slot < count; slot++)
|
||||
{
|
||||
if (!TryReadUInt16(
|
||||
ctx,
|
||||
if (!ctx.TryReadUInt16(
|
||||
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
||||
out var sharp))
|
||||
{
|
||||
@@ -125,30 +121,4 @@ internal static class Gen5ShaderMetadataReader
|
||||
resources);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
@@ -14,12 +13,15 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
private const int ImageDescriptorDwords = 8;
|
||||
private const int SamplerDescriptorDwords = 4;
|
||||
private const int MaxGlobalMemoryBindingBytes = 16 * 1024 * 1024;
|
||||
private const ulong RdnaWaveMask = 0xFFFF_FFFFUL;
|
||||
|
||||
private readonly record struct BufferDescriptor(
|
||||
ulong BaseAddress,
|
||||
uint Stride,
|
||||
uint NumRecords,
|
||||
ulong SizeBytes);
|
||||
ulong SizeBytes,
|
||||
uint NumberFormat,
|
||||
uint DataFormat);
|
||||
|
||||
public static bool TryResolveImageBindings(
|
||||
CpuContext ctx,
|
||||
@@ -41,7 +43,8 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
CpuContext ctx,
|
||||
Gen5ShaderState state,
|
||||
out Gen5ShaderEvaluation evaluation,
|
||||
out string error)
|
||||
out string error,
|
||||
bool resolveVertexInputs = false)
|
||||
{
|
||||
evaluation = default!;
|
||||
error = string.Empty;
|
||||
@@ -60,14 +63,15 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
computeSystemRegisters.ClearStaticValues(scalarRegisters);
|
||||
}
|
||||
|
||||
var execMask = ulong.MaxValue;
|
||||
WriteScalarPair(scalarRegisters, 106, ulong.MaxValue, ref execMask);
|
||||
var execMask = RdnaWaveMask;
|
||||
WriteScalarPair(scalarRegisters, 106, 0, ref execMask);
|
||||
WriteScalarPair(scalarRegisters, 126, execMask, ref execMask);
|
||||
var initialScalarRegisters = (uint[])scalarRegisters.Clone();
|
||||
|
||||
var resolved = new List<Gen5ImageBinding>();
|
||||
var globalMemoryBindings = new List<Gen5GlobalMemoryBinding>();
|
||||
var globalMemoryByAddress = new Dictionary<(uint ScalarAddress, ulong BaseAddress), Gen5GlobalMemoryBinding>();
|
||||
var vertexInputBindings = new List<Gen5VertexInputBinding>();
|
||||
var runtimeScalarRegisters = CollectRuntimeScalarRegisters(state.Program);
|
||||
var scalarRegisterSnapshots = new Dictionary<uint, IReadOnlyList<uint>>();
|
||||
var scalarConditionCode = false;
|
||||
@@ -248,6 +252,42 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
return false;
|
||||
}
|
||||
|
||||
if (resolveVertexInputs &&
|
||||
IsVertexFetchCandidate(instruction, bufferMemory, bufferDescriptor))
|
||||
{
|
||||
if (!TryReadGlobalMemory(
|
||||
ctx,
|
||||
bufferDescriptor.BaseAddress,
|
||||
bufferDescriptor.SizeBytes,
|
||||
out var vertexData))
|
||||
{
|
||||
error =
|
||||
$"vertex-buffer-read-failed pc=0x{instruction.Pc:X} " +
|
||||
$"address=0x{bufferDescriptor.BaseAddress:X16} " +
|
||||
$"bytes={bufferDescriptor.SizeBytes} " +
|
||||
$"stride={bufferDescriptor.Stride} records={bufferDescriptor.NumRecords}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryCreateVertexInputBinding(
|
||||
instruction,
|
||||
bufferMemory,
|
||||
bufferDescriptor,
|
||||
vertexData,
|
||||
(uint)vertexInputBindings.Count,
|
||||
scalarRegisters,
|
||||
out var vertexInputBinding))
|
||||
{
|
||||
error =
|
||||
$"vertex-input-binding-failed pc=0x{instruction.Pc:X} " +
|
||||
$"s{bufferMemory.ScalarResource}";
|
||||
return false;
|
||||
}
|
||||
|
||||
vertexInputBindings.Add(vertexInputBinding);
|
||||
continue;
|
||||
}
|
||||
|
||||
var key = (bufferMemory.ScalarResource, bufferDescriptor.BaseAddress);
|
||||
if (globalMemoryByAddress.TryGetValue(key, out var existingBinding))
|
||||
{
|
||||
@@ -339,10 +379,58 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
resolved,
|
||||
globalMemoryBindings,
|
||||
state.ComputeSystemRegisters,
|
||||
runtimeScalarRegisters);
|
||||
runtimeScalarRegisters,
|
||||
vertexInputBindings);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryCreateVertexInputBinding(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5BufferMemoryControl control,
|
||||
BufferDescriptor descriptor,
|
||||
byte[] data,
|
||||
uint location,
|
||||
uint[] scalarRegisters,
|
||||
out Gen5VertexInputBinding binding)
|
||||
{
|
||||
binding = default!;
|
||||
if (!IsVertexFetchCandidate(instruction, control, descriptor) ||
|
||||
instruction.Sources.Count <= 2 ||
|
||||
!TryEvaluateScalarOperand(instruction.Sources[2], scalarRegisters, out var scalarOffset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bindingData = data;
|
||||
var bindingStride = descriptor.Stride;
|
||||
var bindingOffset = unchecked((uint)control.OffsetBytes + scalarOffset);
|
||||
var bindingDataFormat = descriptor.DataFormat;
|
||||
var bindingNumberFormat = descriptor.NumberFormat;
|
||||
binding = new Gen5VertexInputBinding(
|
||||
instruction.Pc,
|
||||
location,
|
||||
control.DwordCount,
|
||||
bindingDataFormat,
|
||||
bindingNumberFormat,
|
||||
descriptor.BaseAddress,
|
||||
bindingStride,
|
||||
bindingOffset,
|
||||
bindingData);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsVertexFetchCandidate(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5BufferMemoryControl control,
|
||||
BufferDescriptor descriptor) =>
|
||||
control.IndexEnabled &&
|
||||
!control.OffsetEnabled &&
|
||||
control.DwordCount is >= 1 and <= 4 &&
|
||||
descriptor.BaseAddress != 0 &&
|
||||
descriptor.Stride != 0 &&
|
||||
(instruction.Opcode.StartsWith("BufferLoadFormat", StringComparison.Ordinal) ||
|
||||
instruction.Opcode.StartsWith("TBufferLoadFormat", StringComparison.Ordinal));
|
||||
|
||||
private static HashSet<uint> CollectRuntimeScalarRegisters(Gen5ShaderProgram program)
|
||||
{
|
||||
var registers = new HashSet<uint>();
|
||||
@@ -723,7 +811,11 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
"SFF1I32B32" => left == 0 ? uint.MaxValue : (uint)BitOperations.TrailingZeroCount(left),
|
||||
_ => registers[destination.Value] | (1u << ((int)left & 31)),
|
||||
};
|
||||
scalarConditionCode = registers[destination.Value] != 0;
|
||||
if (instruction.Opcode != "SBitset1B32")
|
||||
{
|
||||
scalarConditionCode = registers[destination.Value] != 0;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -749,13 +841,15 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
}
|
||||
case "SSubU32":
|
||||
result = left - right;
|
||||
scalarConditionCode = left >= right;
|
||||
scalarConditionCode = right > left;
|
||||
break;
|
||||
case "SAddI32":
|
||||
result = unchecked((uint)((int)left + (int)right));
|
||||
scalarConditionCode = SignedAddOverflow(left, right, result);
|
||||
break;
|
||||
case "SSubI32":
|
||||
result = unchecked((uint)((int)left - (int)right));
|
||||
scalarConditionCode = SignedSubOverflow(left, right, result);
|
||||
break;
|
||||
case "SAddcU32":
|
||||
{
|
||||
@@ -766,23 +860,27 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
}
|
||||
case "SSubbU32":
|
||||
{
|
||||
var borrow = scalarConditionCode ? 0UL : 1UL;
|
||||
var borrow = scalarConditionCode ? 1UL : 0UL;
|
||||
var subtrahend = (ulong)right + borrow;
|
||||
result = unchecked(left - (uint)subtrahend);
|
||||
scalarConditionCode = left >= subtrahend;
|
||||
scalarConditionCode = subtrahend > left;
|
||||
break;
|
||||
}
|
||||
case "SMinI32":
|
||||
result = unchecked((uint)Math.Min((int)left, (int)right));
|
||||
scalarConditionCode = (int)left < (int)right;
|
||||
break;
|
||||
case "SMinU32":
|
||||
result = Math.Min(left, right);
|
||||
scalarConditionCode = left < right;
|
||||
break;
|
||||
case "SMaxI32":
|
||||
result = unchecked((uint)Math.Max((int)left, (int)right));
|
||||
scalarConditionCode = (int)left > (int)right;
|
||||
break;
|
||||
case "SMaxU32":
|
||||
result = Math.Max(left, right);
|
||||
scalarConditionCode = left > right;
|
||||
break;
|
||||
case "SCselectB32":
|
||||
result = scalarConditionCode ? left : right;
|
||||
@@ -846,6 +944,7 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
var offset = (int)right & 31;
|
||||
var width = Math.Min(((int)right >> 16) & 0x7F, 32 - offset);
|
||||
result = width == 0 ? 0 : left >> offset & (uint.MaxValue >> (32 - width));
|
||||
scalarConditionCode = result != 0;
|
||||
break;
|
||||
}
|
||||
case "SBfeI32":
|
||||
@@ -855,23 +954,41 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
result = width == 0
|
||||
? 0
|
||||
: unchecked((uint)(((int)(left << (32 - width - offset))) >> (32 - width)));
|
||||
scalarConditionCode = result != 0;
|
||||
break;
|
||||
}
|
||||
case "SAbsdiffI32":
|
||||
result = unchecked((uint)Math.Abs((long)(int)left - (int)right));
|
||||
scalarConditionCode = result != 0;
|
||||
break;
|
||||
case "SLshl1AddU32":
|
||||
result = (left << 1) + right;
|
||||
break;
|
||||
{
|
||||
var wide = ((ulong)left << 1) + right;
|
||||
result = (uint)wide;
|
||||
scalarConditionCode = wide > uint.MaxValue;
|
||||
break;
|
||||
}
|
||||
case "SLshl2AddU32":
|
||||
result = (left << 2) + right;
|
||||
break;
|
||||
{
|
||||
var wide = ((ulong)left << 2) + right;
|
||||
result = (uint)wide;
|
||||
scalarConditionCode = wide > uint.MaxValue;
|
||||
break;
|
||||
}
|
||||
case "SLshl3AddU32":
|
||||
result = (left << 3) + right;
|
||||
break;
|
||||
{
|
||||
var wide = ((ulong)left << 3) + right;
|
||||
result = (uint)wide;
|
||||
scalarConditionCode = wide > uint.MaxValue;
|
||||
break;
|
||||
}
|
||||
case "SLshl4AddU32":
|
||||
result = (left << 4) + right;
|
||||
break;
|
||||
{
|
||||
var wide = ((ulong)left << 4) + right;
|
||||
result = (uint)wide;
|
||||
scalarConditionCode = wide > uint.MaxValue;
|
||||
break;
|
||||
}
|
||||
case "SPackLlB32B16":
|
||||
result = (left & 0xFFFFu) | (right << 16);
|
||||
break;
|
||||
@@ -913,7 +1030,8 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
"SNandSaveexecB64" or
|
||||
"SNorSaveexecB64" or
|
||||
"SXnorSaveexecB64" or
|
||||
"SAndn1SaveexecB64"))
|
||||
"SAndn1SaveexecB64" or
|
||||
"SOrn1SaveexecB64"))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -941,18 +1059,19 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
"SAndSaveexecB64" => oldExec & source,
|
||||
"SOrSaveexecB64" => oldExec | source,
|
||||
"SXorSaveexecB64" => oldExec ^ source,
|
||||
"SAndn1SaveexecB64" => ~oldExec & source,
|
||||
"SAndn2SaveexecB64" => oldExec & ~source,
|
||||
"SOrn2SaveexecB64" => oldExec | ~source,
|
||||
"SNandSaveexecB64" => ~(oldExec & source),
|
||||
"SNorSaveexecB64" => ~(oldExec | source),
|
||||
"SAndn1SaveexecB64" => ~source & oldExec,
|
||||
"SAndn2SaveexecB64" => source & ~oldExec,
|
||||
"SOrn1SaveexecB64" => ~source | oldExec,
|
||||
"SOrn2SaveexecB64" => source | ~oldExec,
|
||||
"SNandSaveexecB64" => ~(source & oldExec),
|
||||
"SNorSaveexecB64" => ~(source | oldExec),
|
||||
_ => ~(oldExec ^ source),
|
||||
};
|
||||
|
||||
WriteScalarPair(registers, destination.Value, oldExec, ref execMask);
|
||||
execMask = newExec;
|
||||
execMask = MaskWaveValue(newExec);
|
||||
WriteScalarPair(registers, 126, execMask, ref execMask);
|
||||
scalarConditionCode = newExec != 0;
|
||||
scalarConditionCode = execMask != 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1000,6 +1119,11 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
return;
|
||||
}
|
||||
|
||||
if (destination == 126)
|
||||
{
|
||||
value = MaskWaveValue(value);
|
||||
}
|
||||
|
||||
registers[destination] = (uint)value;
|
||||
registers[destination + 1] = (uint)(value >> 32);
|
||||
if (destination == 126)
|
||||
@@ -1008,6 +1132,14 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
}
|
||||
}
|
||||
|
||||
private static ulong MaskWaveValue(ulong value) => value & RdnaWaveMask;
|
||||
|
||||
private static bool SignedAddOverflow(uint left, uint right, uint result) =>
|
||||
((left ^ result) & (right ^ result) & 0x80000000u) != 0;
|
||||
|
||||
private static bool SignedSubOverflow(uint left, uint right, uint result) =>
|
||||
((left ^ right) & (left ^ result) & 0x80000000u) != 0;
|
||||
|
||||
private static bool TryExecuteScalarCompare(
|
||||
Gen5ShaderInstruction instruction,
|
||||
uint[] registers,
|
||||
@@ -1272,8 +1404,7 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!TryReadUInt32(
|
||||
ctx,
|
||||
if (!ctx.TryReadUInt32(
|
||||
address + (ulong)(index * sizeof(uint)),
|
||||
out var value) &&
|
||||
!TryReadUserDataScalarLoad(
|
||||
@@ -1329,7 +1460,7 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
word2 == 0 &&
|
||||
word3 == 0)
|
||||
{
|
||||
descriptor = new BufferDescriptor(0, 0, 0, 0);
|
||||
descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1341,19 +1472,64 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
return false;
|
||||
}
|
||||
|
||||
descriptor = new BufferDescriptor(0, 0, 0, 0);
|
||||
descriptor = new BufferDescriptor(0, 0, 0, 0, 0, 0);
|
||||
return true;
|
||||
}
|
||||
|
||||
var baseAddress = word0 | ((ulong)(word1 & 0x0FFFu) << 32);
|
||||
var baseAddress = word0 | ((ulong)(word1 & 0xFFFFu) << 32);
|
||||
var stride = (word1 >> 16) & 0x3FFFu;
|
||||
var unifiedFormat = (word3 >> 12) & 0x7Fu;
|
||||
var (dataFormat, numberFormat) =
|
||||
DecodeGfx10BufferFormat(unifiedFormat);
|
||||
var sizeBytes = stride == 0
|
||||
? word2
|
||||
: (ulong)stride * word2;
|
||||
descriptor = new BufferDescriptor(baseAddress, stride, word2, sizeBytes);
|
||||
descriptor = new BufferDescriptor(baseAddress, stride, word2, sizeBytes, numberFormat, dataFormat);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static (uint DataFormat, uint NumberFormat)
|
||||
DecodeGfx10BufferFormat(uint format) =>
|
||||
format switch
|
||||
{
|
||||
0 => (0, 0),
|
||||
>= 1 and <= 6 => (1, format - 1),
|
||||
>= 7 and <= 13 => (2, DecodeUnifiedNumber(format - 7, 7)),
|
||||
>= 14 and <= 19 => (3, format - 14),
|
||||
>= 20 and <= 22 => (4, DecodeIntegerOrFloatNumber(format - 20)),
|
||||
>= 23 and <= 29 => (5, DecodeUnifiedNumber(format - 23, 7)),
|
||||
>= 30 and <= 36 => (6, DecodeUnifiedNumber(format - 30, 7)),
|
||||
>= 37 and <= 43 => (7, DecodeUnifiedNumber(format - 37, 7)),
|
||||
>= 44 and <= 49 => (8, format - 44),
|
||||
>= 50 and <= 55 => (9, format - 50),
|
||||
>= 56 and <= 61 => (10, format - 56),
|
||||
>= 62 and <= 64 => (11, DecodeIntegerOrFloatNumber(format - 62)),
|
||||
>= 65 and <= 71 => (12, DecodeUnifiedNumber(format - 65, 7)),
|
||||
>= 72 and <= 74 => (13, DecodeIntegerOrFloatNumber(format - 72)),
|
||||
>= 75 and <= 77 => (14, DecodeIntegerOrFloatNumber(format - 75)),
|
||||
128 => (1, 9),
|
||||
129 => (3, 9),
|
||||
130 => (10, 9),
|
||||
132 => (34, 7),
|
||||
133 => (16, 0),
|
||||
134 => (17, 0),
|
||||
135 => (18, 0),
|
||||
136 => (19, 0),
|
||||
140 => (4, 7),
|
||||
_ => (0, 0),
|
||||
};
|
||||
|
||||
private static uint DecodeUnifiedNumber(uint offset, uint formatCount) =>
|
||||
offset == formatCount - 1 ? 7u : offset;
|
||||
|
||||
private static uint DecodeIntegerOrFloatNumber(uint offset) =>
|
||||
offset switch
|
||||
{
|
||||
0 => 4,
|
||||
1 => 5,
|
||||
_ => 7,
|
||||
};
|
||||
|
||||
private static bool TryReadUserDataScalarLoad(
|
||||
Gen5ShaderState state,
|
||||
Gen5ShaderInstruction instruction,
|
||||
@@ -1549,16 +1725,4 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
|
||||
opcode.StartsWith("ImageGather", StringComparison.Ordinal);
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
@@ -13,6 +14,14 @@ internal static class Gen5ShaderTranslator
|
||||
private const int MaxInstructions = 4096;
|
||||
private const int MinimumUserDataDwords = 16;
|
||||
private const int MaximumUserDataDwords = 256;
|
||||
private static readonly ConditionalWeakTable<object, ShaderDecodeCache> _decodeCaches = new();
|
||||
|
||||
private sealed class ShaderDecodeCache
|
||||
{
|
||||
public object Gate { get; } = new();
|
||||
public Dictionary<ulong, Gen5ShaderProgram> Programs { get; } = new();
|
||||
public Dictionary<ulong, Gen5ShaderMetadata?> Metadata { get; } = new();
|
||||
}
|
||||
|
||||
private static readonly uint[] FullscreenBarycentricEs =
|
||||
[
|
||||
@@ -117,16 +126,51 @@ internal static class Gen5ShaderTranslator
|
||||
uint userDataScalarRegisterBase = 0)
|
||||
{
|
||||
state = default!;
|
||||
if (!TryDecodeProgram(ctx, shaderAddress, out var program, out error))
|
||||
error = string.Empty;
|
||||
var cache = _decodeCaches.GetValue(ctx.Memory, static _ => new ShaderDecodeCache());
|
||||
Gen5ShaderProgram? program;
|
||||
lock (cache.Gate)
|
||||
{
|
||||
return false;
|
||||
cache.Programs.TryGetValue(shaderAddress, out program);
|
||||
}
|
||||
|
||||
if (program is null)
|
||||
{
|
||||
if (!TryDecodeProgram(ctx, shaderAddress, out program, out error))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (cache.Gate)
|
||||
{
|
||||
cache.Programs.TryAdd(shaderAddress, program);
|
||||
}
|
||||
}
|
||||
|
||||
Gen5ShaderMetadata? metadata = null;
|
||||
if (shaderHeaderAddress != 0 &&
|
||||
Gen5ShaderMetadataReader.TryRead(ctx, shaderHeaderAddress, out var decodedMetadata))
|
||||
if (shaderHeaderAddress != 0)
|
||||
{
|
||||
metadata = decodedMetadata;
|
||||
var metadataCached = false;
|
||||
lock (cache.Gate)
|
||||
{
|
||||
metadataCached = cache.Metadata.TryGetValue(shaderHeaderAddress, out metadata);
|
||||
}
|
||||
|
||||
if (!metadataCached)
|
||||
{
|
||||
if (Gen5ShaderMetadataReader.TryRead(
|
||||
ctx,
|
||||
shaderHeaderAddress,
|
||||
out var decodedMetadata))
|
||||
{
|
||||
metadata = decodedMetadata;
|
||||
}
|
||||
|
||||
lock (cache.Gate)
|
||||
{
|
||||
cache.Metadata.TryAdd(shaderHeaderAddress, metadata);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var userData = new uint[GetUserDataDwordCount(metadata)];
|
||||
@@ -250,7 +294,7 @@ internal static class Gen5ShaderTranslator
|
||||
var instructionCount = 0;
|
||||
for (uint pc = 0; instructionCount < MaxInstructions;)
|
||||
{
|
||||
if (!TryReadUInt32(ctx, address + pc, out var word))
|
||||
if (!ctx.TryReadUInt32(address + pc, out var word))
|
||||
{
|
||||
error = $"read-failed pc=0x{pc:X}";
|
||||
return false;
|
||||
@@ -273,7 +317,7 @@ internal static class Gen5ShaderTranslator
|
||||
words[0] = word;
|
||||
for (uint wordIndex = 1; wordIndex < sizeDwords; wordIndex++)
|
||||
{
|
||||
if (!TryReadUInt32(ctx, address + pc + wordIndex * sizeof(uint), out words[wordIndex]))
|
||||
if (!ctx.TryReadUInt32(address + pc + wordIndex * sizeof(uint), out words[wordIndex]))
|
||||
{
|
||||
error = $"read-failed pc=0x{pc + wordIndex * sizeof(uint):X}";
|
||||
return false;
|
||||
@@ -353,7 +397,7 @@ internal static class Gen5ShaderTranslator
|
||||
case 0x34:
|
||||
case 0x35:
|
||||
encoding = Gen5ShaderEncoding.Vop3;
|
||||
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var vop3Extra))
|
||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var vop3Extra))
|
||||
{
|
||||
error = $"vop3-extra-read-failed pc=0x{pc:X}";
|
||||
return false;
|
||||
@@ -374,7 +418,7 @@ internal static class Gen5ShaderTranslator
|
||||
return DecodeFlat(word, out name, out sizeDwords, out error);
|
||||
case 0x38:
|
||||
encoding = Gen5ShaderEncoding.Mubuf;
|
||||
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mubufExtra))
|
||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var mubufExtra))
|
||||
{
|
||||
error = $"mubuf-extra-read-failed pc=0x{pc:X}";
|
||||
return false;
|
||||
@@ -383,7 +427,7 @@ internal static class Gen5ShaderTranslator
|
||||
return DecodeMubuf(word, mubufExtra, out name, out sizeDwords, out error);
|
||||
case 0x3A:
|
||||
encoding = Gen5ShaderEncoding.Mtbuf;
|
||||
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mtbufExtra))
|
||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var mtbufExtra))
|
||||
{
|
||||
error = $"mtbuf-extra-read-failed pc=0x{pc:X}";
|
||||
return false;
|
||||
@@ -452,6 +496,7 @@ internal static class Gen5ShaderTranslator
|
||||
0x2A => "SNorSaveexecB64",
|
||||
0x2B => "SXnorSaveexecB64",
|
||||
0x37 => "SAndn1SaveexecB64",
|
||||
0x38 => "SOrn1SaveexecB64",
|
||||
_ => string.Empty,
|
||||
};
|
||||
|
||||
@@ -1696,19 +1741,6 @@ internal static class Gen5ShaderTranslator
|
||||
counts[key] = count + 1;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private readonly record struct ShaderDecodeInfo(
|
||||
int InstructionCount,
|
||||
Dictionary<string, int> Counts,
|
||||
|
||||
@@ -133,9 +133,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
SpirvOp.ConvertFToU,
|
||||
_uintType,
|
||||
GetFloatSource(instruction, 0));
|
||||
var offset = _module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
_uintType,
|
||||
var offset = ShiftLeftLogical(
|
||||
BitwiseAnd(GetRawSource(instruction, 1), UInt(3)),
|
||||
UInt(3));
|
||||
result = _module.AddInstruction(
|
||||
@@ -214,12 +212,26 @@ internal static partial class Gen5SpirvTranslator
|
||||
case "VSinF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(13, _floatType, GetFloatSource(instruction, 0)));
|
||||
Ext(
|
||||
13,
|
||||
_floatType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.FMul,
|
||||
_floatType,
|
||||
GetFloatSource(instruction, 0),
|
||||
Float(MathF.Tau))));
|
||||
break;
|
||||
case "VCosF32":
|
||||
result = EmitFloatResult(
|
||||
instruction,
|
||||
Ext(14, _floatType, GetFloatSource(instruction, 0)));
|
||||
Ext(
|
||||
14,
|
||||
_floatType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.FMul,
|
||||
_floatType,
|
||||
GetFloatSource(instruction, 0),
|
||||
Float(MathF.Tau))));
|
||||
break;
|
||||
case "VAddF32":
|
||||
result = EmitFloatBinary(instruction, SpirvOp.FAdd);
|
||||
@@ -352,9 +364,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_uintType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ShiftRightLogical,
|
||||
_ulongType,
|
||||
ShiftRightLogical64(
|
||||
product,
|
||||
_module.Constant64(_ulongType, 32)));
|
||||
break;
|
||||
@@ -417,21 +427,12 @@ internal static partial class Gen5SpirvTranslator
|
||||
var reverse = instruction.Opcode == "VAshrrevI32";
|
||||
var left = GetRawSource(instruction, reverse ? 1 : 0);
|
||||
var right = GetRawSource(instruction, reverse ? 0 : 1);
|
||||
right = BitwiseAnd(right, UInt(31));
|
||||
result = Bitcast(
|
||||
_uintType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ShiftRightArithmetic,
|
||||
_intType,
|
||||
Bitcast(_intType, left),
|
||||
right));
|
||||
result = ShiftRightArithmetic(left, right);
|
||||
break;
|
||||
}
|
||||
case "VLshlAddU32":
|
||||
{
|
||||
var shifted = _module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
_uintType,
|
||||
var shifted = ShiftLeftLogical(
|
||||
GetRawSource(instruction, 0),
|
||||
BitwiseAnd(GetRawSource(instruction, 1), UInt(31)));
|
||||
result = IAdd(shifted, GetRawSource(instruction, 2));
|
||||
@@ -439,9 +440,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
}
|
||||
case "VLshlOrU32":
|
||||
{
|
||||
var shifted = _module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
_uintType,
|
||||
var shifted = ShiftLeftLogical(
|
||||
GetRawSource(instruction, 0),
|
||||
BitwiseAnd(GetRawSource(instruction, 1), UInt(31)));
|
||||
result = BitwiseOr(
|
||||
@@ -474,11 +473,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
var added = IAdd(
|
||||
GetRawSource(instruction, 0),
|
||||
GetRawSource(instruction, 1));
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
_uintType,
|
||||
added,
|
||||
BitwiseAnd(GetRawSource(instruction, 2), UInt(31)));
|
||||
result = ShiftLeftLogical(added, GetRawSource(instruction, 2));
|
||||
break;
|
||||
}
|
||||
case "VAdd3U32":
|
||||
@@ -654,13 +649,29 @@ internal static partial class Gen5SpirvTranslator
|
||||
width);
|
||||
break;
|
||||
}
|
||||
case "VBfiB32":
|
||||
{
|
||||
var mask = GetRawSource(instruction, 0);
|
||||
var insert = GetRawSource(instruction, 1);
|
||||
var source = GetRawSource(instruction, 2);
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.BitwiseOr,
|
||||
_uintType,
|
||||
BitwiseAnd(mask, insert),
|
||||
BitwiseAnd(
|
||||
_module.AddInstruction(SpirvOp.Not, _uintType, mask),
|
||||
source));
|
||||
break;
|
||||
}
|
||||
case "VCvtPkrtzF16F32":
|
||||
{
|
||||
var first = TruncateFloat32ForPack(GetFloatSource(instruction, 0));
|
||||
var second = TruncateFloat32ForPack(GetFloatSource(instruction, 1));
|
||||
var vector = _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
_vec2Type,
|
||||
GetFloatSource(instruction, 0),
|
||||
GetFloatSource(instruction, 1));
|
||||
first,
|
||||
second);
|
||||
result = Ext(58, _uintType, vector);
|
||||
break;
|
||||
}
|
||||
@@ -964,6 +975,16 @@ internal static partial class Gen5SpirvTranslator
|
||||
StoreS(destination, result);
|
||||
Store(_scc, IsNotZero(result));
|
||||
return true;
|
||||
case "SBitset1B32":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.BitFieldInsert,
|
||||
_uintType,
|
||||
LoadS(destination),
|
||||
UInt(1),
|
||||
BitwiseAnd(left, UInt(31)),
|
||||
UInt(1));
|
||||
StoreS(destination, result);
|
||||
return true;
|
||||
default:
|
||||
{
|
||||
if (instruction.Sources.Count < 2)
|
||||
@@ -990,13 +1011,14 @@ internal static partial class Gen5SpirvTranslator
|
||||
left,
|
||||
right);
|
||||
Store(_scc, _module.AddInstruction(
|
||||
SpirvOp.UGreaterThanEqual,
|
||||
SpirvOp.UGreaterThan,
|
||||
_boolType,
|
||||
left,
|
||||
right));
|
||||
right,
|
||||
left));
|
||||
break;
|
||||
case "SAddI32":
|
||||
result = IAdd(left, right);
|
||||
Store(_scc, SignedAddOverflow(left, right, result));
|
||||
break;
|
||||
case "SSubI32":
|
||||
result = _module.AddInstruction(
|
||||
@@ -1004,6 +1026,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
_uintType,
|
||||
left,
|
||||
right);
|
||||
Store(_scc, SignedSubOverflow(left, right, result));
|
||||
break;
|
||||
case "SAddcU32":
|
||||
{
|
||||
@@ -1040,8 +1063,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
SpirvOp.Select,
|
||||
_uintType,
|
||||
Load(_boolType, _scc),
|
||||
UInt(0),
|
||||
UInt(1));
|
||||
UInt(1),
|
||||
UInt(0));
|
||||
var partial = _module.AddInstruction(
|
||||
SpirvOp.ISub,
|
||||
_uintType,
|
||||
@@ -1052,23 +1075,31 @@ internal static partial class Gen5SpirvTranslator
|
||||
_uintType,
|
||||
partial,
|
||||
borrow);
|
||||
var firstNoBorrow = _module.AddInstruction(
|
||||
SpirvOp.UGreaterThanEqual,
|
||||
var firstBorrow = _module.AddInstruction(
|
||||
SpirvOp.UGreaterThan,
|
||||
_boolType,
|
||||
left,
|
||||
right);
|
||||
var secondNoBorrow = _module.AddInstruction(
|
||||
SpirvOp.UGreaterThanEqual,
|
||||
right,
|
||||
left);
|
||||
var secondBorrow = _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
partial,
|
||||
borrow);
|
||||
_module.AddInstruction(
|
||||
SpirvOp.IEqual,
|
||||
_boolType,
|
||||
borrow,
|
||||
UInt(1)),
|
||||
_module.AddInstruction(
|
||||
SpirvOp.IEqual,
|
||||
_boolType,
|
||||
right,
|
||||
left));
|
||||
Store(
|
||||
_scc,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
SpirvOp.LogicalOr,
|
||||
_boolType,
|
||||
firstNoBorrow,
|
||||
secondNoBorrow));
|
||||
firstBorrow,
|
||||
secondBorrow));
|
||||
break;
|
||||
}
|
||||
case "SMulI32":
|
||||
@@ -1080,6 +1111,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
break;
|
||||
case "SAndB32":
|
||||
result = BitwiseAnd(left, right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SOrB32":
|
||||
result = _module.AddInstruction(
|
||||
@@ -1087,6 +1119,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
_uintType,
|
||||
left,
|
||||
right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SXorB32":
|
||||
result = _module.AddInstruction(
|
||||
@@ -1094,32 +1127,64 @@ internal static partial class Gen5SpirvTranslator
|
||||
_uintType,
|
||||
left,
|
||||
right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SAndn2B32":
|
||||
result = BitwiseAnd(
|
||||
left,
|
||||
_module.AddInstruction(SpirvOp.Not, _uintType, right));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SLshlB32":
|
||||
case "SOrn2B32":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
SpirvOp.BitwiseOr,
|
||||
_uintType,
|
||||
left,
|
||||
BitwiseAnd(right, UInt(31)));
|
||||
_module.AddInstruction(SpirvOp.Not, _uintType, right));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SNandB32":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_uintType,
|
||||
BitwiseAnd(left, right));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SNorB32":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_uintType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitwiseOr,
|
||||
_uintType,
|
||||
left,
|
||||
right));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SXnorB32":
|
||||
result = _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_uintType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitwiseXor,
|
||||
_uintType,
|
||||
left,
|
||||
right));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SLshlB32":
|
||||
result = ShiftLeftLogical(left, right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SLshrB32":
|
||||
result = ShiftRightLogical(
|
||||
left,
|
||||
BitwiseAnd(right, UInt(31)));
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SAshrI32":
|
||||
result = Bitcast(
|
||||
_uintType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ShiftRightArithmetic,
|
||||
_intType,
|
||||
Bitcast(_intType, left),
|
||||
BitwiseAnd(right, UInt(31))));
|
||||
result = ShiftRightArithmetic(left, right);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
case "SBfmB32":
|
||||
result = _module.AddInstruction(
|
||||
@@ -1162,6 +1227,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
left,
|
||||
offset,
|
||||
width);
|
||||
Store(_scc, IsNotZero(result));
|
||||
break;
|
||||
}
|
||||
case "SCselectB32":
|
||||
@@ -1174,9 +1240,47 @@ internal static partial class Gen5SpirvTranslator
|
||||
break;
|
||||
case "SMinU32":
|
||||
result = Ext(38, _uintType, left, right);
|
||||
Store(
|
||||
_scc,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ULessThan,
|
||||
_boolType,
|
||||
left,
|
||||
right));
|
||||
break;
|
||||
case "SMinI32":
|
||||
result = Bitcast(
|
||||
_uintType,
|
||||
Ext(39, _intType, Bitcast(_intType, left), Bitcast(_intType, right)));
|
||||
Store(
|
||||
_scc,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.SLessThan,
|
||||
_boolType,
|
||||
Bitcast(_intType, left),
|
||||
Bitcast(_intType, right)));
|
||||
break;
|
||||
case "SMaxU32":
|
||||
result = Ext(41, _uintType, left, right);
|
||||
Store(
|
||||
_scc,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.UGreaterThan,
|
||||
_boolType,
|
||||
left,
|
||||
right));
|
||||
break;
|
||||
case "SMaxI32":
|
||||
result = Bitcast(
|
||||
_uintType,
|
||||
Ext(42, _intType, Bitcast(_intType, left), Bitcast(_intType, right)));
|
||||
Store(
|
||||
_scc,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.SGreaterThan,
|
||||
_boolType,
|
||||
Bitcast(_intType, left),
|
||||
Bitcast(_intType, right)));
|
||||
break;
|
||||
case "SLshl1AddU32":
|
||||
case "SLshl2AddU32":
|
||||
@@ -1185,11 +1289,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
{
|
||||
var shift = (uint)(instruction.Opcode[5] - '0');
|
||||
result = IAdd(
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
_uintType,
|
||||
left,
|
||||
UInt(shift)),
|
||||
ShiftLeftLogical(left, UInt(shift)),
|
||||
right);
|
||||
break;
|
||||
}
|
||||
@@ -1325,17 +1425,55 @@ internal static partial class Gen5SpirvTranslator
|
||||
"SXorSaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseXor, _ulongType, oldExec, left),
|
||||
"SAndn2SaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd, _ulongType, oldExec, notLeft),
|
||||
"SAndn1SaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd,
|
||||
_ulongType,
|
||||
left,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_ulongType,
|
||||
oldExec),
|
||||
left),
|
||||
oldExec)),
|
||||
"SAndn1SaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd,
|
||||
_ulongType,
|
||||
notLeft,
|
||||
oldExec),
|
||||
"SOrn1SaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseOr,
|
||||
_ulongType,
|
||||
notLeft,
|
||||
oldExec),
|
||||
"SOrn2SaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.BitwiseOr, _ulongType, oldExec, notLeft),
|
||||
SpirvOp.BitwiseOr,
|
||||
_ulongType,
|
||||
left,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_ulongType,
|
||||
oldExec)),
|
||||
"SNandSaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_ulongType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd,
|
||||
_ulongType,
|
||||
left,
|
||||
oldExec)),
|
||||
"SNorSaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_ulongType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitwiseOr,
|
||||
_ulongType,
|
||||
left,
|
||||
oldExec)),
|
||||
"SXnorSaveexecB64" => _module.AddInstruction(
|
||||
SpirvOp.Not,
|
||||
_ulongType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitwiseXor,
|
||||
_ulongType,
|
||||
left,
|
||||
oldExec)),
|
||||
_ => 0u,
|
||||
};
|
||||
if (newExec == 0)
|
||||
@@ -1362,14 +1500,10 @@ internal static partial class Gen5SpirvTranslator
|
||||
var shift = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_ulongType,
|
||||
BitwiseAnd(GetRawSource(instruction, 1), UInt(63)));
|
||||
var shiftedValue = _module.AddInstruction(
|
||||
instruction.Opcode == "SLshlB64"
|
||||
? SpirvOp.ShiftLeftLogical
|
||||
: SpirvOp.ShiftRightLogical,
|
||||
_ulongType,
|
||||
left,
|
||||
shift);
|
||||
GetRawSource(instruction, 1));
|
||||
var shiftedValue = instruction.Opcode == "SLshlB64"
|
||||
? ShiftLeftLogical64(left, shift)
|
||||
: ShiftRightLogical64(left, shift);
|
||||
StoreS64(destination, shiftedValue);
|
||||
Store(_scc, IsNotZero64(shiftedValue));
|
||||
return true;
|
||||
@@ -1398,21 +1532,71 @@ internal static partial class Gen5SpirvTranslator
|
||||
_uintType,
|
||||
requestedWidth,
|
||||
remaining);
|
||||
var extracted = instruction.Opcode == "SBfeI64"
|
||||
? Bitcast(
|
||||
var offset64 = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_ulongType,
|
||||
offset);
|
||||
var width64 = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_ulongType,
|
||||
width);
|
||||
var one64 = _module.Constant64(_ulongType, 1);
|
||||
var shifted = ShiftRightLogical64(left, offset64);
|
||||
var partialMask = _module.AddInstruction(
|
||||
SpirvOp.ISub,
|
||||
_ulongType,
|
||||
ShiftLeftLogical64(one64, width64),
|
||||
one64);
|
||||
var fullWidth = _module.AddInstruction(
|
||||
SpirvOp.IEqual,
|
||||
_boolType,
|
||||
width,
|
||||
UInt(64));
|
||||
var mask = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_ulongType,
|
||||
fullWidth,
|
||||
_module.Constant64(_ulongType, ulong.MaxValue),
|
||||
partialMask);
|
||||
var extracted = _module.AddInstruction(
|
||||
SpirvOp.BitwiseAnd,
|
||||
_ulongType,
|
||||
shifted,
|
||||
mask);
|
||||
if (instruction.Opcode == "SBfeI64")
|
||||
{
|
||||
var signShift = _module.AddInstruction(
|
||||
SpirvOp.ISub,
|
||||
_uintType,
|
||||
width,
|
||||
UInt(1));
|
||||
var signBit = ShiftLeftLogical64(
|
||||
one64,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_ulongType,
|
||||
signShift));
|
||||
var signExtended = _module.AddInstruction(
|
||||
SpirvOp.ISub,
|
||||
_ulongType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.BitFieldSExtract,
|
||||
_longType,
|
||||
Bitcast(_longType, left),
|
||||
offset,
|
||||
width))
|
||||
: _module.AddInstruction(
|
||||
SpirvOp.BitFieldUExtract,
|
||||
SpirvOp.BitwiseXor,
|
||||
_ulongType,
|
||||
extracted,
|
||||
signBit),
|
||||
signBit);
|
||||
extracted = _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_ulongType,
|
||||
left,
|
||||
offset,
|
||||
width);
|
||||
_module.AddInstruction(
|
||||
SpirvOp.IEqual,
|
||||
_boolType,
|
||||
width,
|
||||
UInt(0)),
|
||||
_module.Constant64(_ulongType, 0),
|
||||
signExtended);
|
||||
}
|
||||
|
||||
StoreS64(destination, extracted);
|
||||
Store(_scc, IsNotZero64(extracted));
|
||||
return true;
|
||||
@@ -1495,6 +1679,22 @@ internal static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
StoreS64(destination, value);
|
||||
if (instruction.Opcode is
|
||||
"SNotB64" or
|
||||
"SAndB64" or
|
||||
"SOrB64" or
|
||||
"SXorB64" or
|
||||
"SAndn1B64" or
|
||||
"SAndn2B64" or
|
||||
"SOrn1B64" or
|
||||
"SOrn2B64" or
|
||||
"SNandB64" or
|
||||
"SNorB64" or
|
||||
"SXnorB64")
|
||||
{
|
||||
Store(_scc, IsNotZero64(value));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1615,11 +1815,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
SpirvOp.UConvert,
|
||||
_ulongType,
|
||||
LoadS(register + 1));
|
||||
high = _module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
_ulongType,
|
||||
high,
|
||||
_module.Constant64(_ulongType, 32));
|
||||
high = ShiftLeftLogical64(high, _module.Constant64(_ulongType, 32));
|
||||
return _module.AddInstruction(SpirvOp.BitwiseOr, _ulongType, low, high);
|
||||
}
|
||||
|
||||
@@ -1628,9 +1824,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
StoreS(
|
||||
register,
|
||||
_module.AddInstruction(SpirvOp.UConvert, _uintType, value));
|
||||
var high = _module.AddInstruction(
|
||||
SpirvOp.ShiftRightLogical,
|
||||
_ulongType,
|
||||
var high = ShiftRightLogical64(
|
||||
value,
|
||||
_module.Constant64(_ulongType, 32));
|
||||
StoreS(
|
||||
@@ -1682,12 +1876,19 @@ internal static partial class Gen5SpirvTranslator
|
||||
{
|
||||
var left = GetRawSource(instruction, reverse ? 1 : 0);
|
||||
var right = GetRawSource(instruction, reverse ? 0 : 1);
|
||||
if (operation is
|
||||
SpirvOp.ShiftLeftLogical or
|
||||
SpirvOp.ShiftRightLogical or
|
||||
SpirvOp.ShiftRightArithmetic)
|
||||
if (operation == SpirvOp.ShiftLeftLogical)
|
||||
{
|
||||
right = BitwiseAnd(right, UInt(31));
|
||||
return ShiftLeftLogical(left, right);
|
||||
}
|
||||
|
||||
if (operation == SpirvOp.ShiftRightLogical)
|
||||
{
|
||||
return ShiftRightLogical(left, right);
|
||||
}
|
||||
|
||||
if (operation == SpirvOp.ShiftRightArithmetic)
|
||||
{
|
||||
return ShiftRightArithmetic(left, right);
|
||||
}
|
||||
|
||||
return _module.AddInstruction(operation, _uintType, left, right);
|
||||
@@ -1986,21 +2187,13 @@ internal static partial class Gen5SpirvTranslator
|
||||
_boolType,
|
||||
localLane,
|
||||
UInt(8));
|
||||
var lowShift = _module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
_uintType,
|
||||
localLane,
|
||||
UInt(2));
|
||||
var lowShift = ShiftLeftLogical(localLane, UInt(2));
|
||||
var highLane = _module.AddInstruction(
|
||||
SpirvOp.ISub,
|
||||
_uintType,
|
||||
localLane,
|
||||
UInt(8));
|
||||
var highShift = _module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
_uintType,
|
||||
highLane,
|
||||
UInt(2));
|
||||
var highShift = ShiftLeftLogical(highLane, UInt(2));
|
||||
var lowSelector = BitwiseAnd(
|
||||
ShiftRightLogical(selectorLow, lowShift),
|
||||
UInt(15));
|
||||
@@ -2061,6 +2254,14 @@ internal static partial class Gen5SpirvTranslator
|
||||
return Bitcast(_uintType, value);
|
||||
}
|
||||
|
||||
private uint TruncateFloat32ForPack(uint value)
|
||||
{
|
||||
var raw = BitwiseAnd(
|
||||
Bitcast(_uintType, value),
|
||||
UInt(0xFFFF_E000));
|
||||
return Bitcast(_floatType, raw);
|
||||
}
|
||||
|
||||
private uint Ext(uint operation, uint resultType, params uint[] operands)
|
||||
{
|
||||
var values = new uint[2 + operands.Length];
|
||||
@@ -2080,6 +2281,53 @@ internal static partial class Gen5SpirvTranslator
|
||||
value,
|
||||
_module.Constant64(_ulongType, 0));
|
||||
|
||||
private uint SignBit(uint value) =>
|
||||
ShiftRightLogical(value, UInt(31));
|
||||
|
||||
private uint SignedAddOverflow(uint left, uint right, uint result)
|
||||
{
|
||||
var leftSign = SignBit(left);
|
||||
var rightSign = SignBit(right);
|
||||
var resultSign = SignBit(result);
|
||||
var sameSourceSign = _module.AddInstruction(
|
||||
SpirvOp.IEqual,
|
||||
_boolType,
|
||||
leftSign,
|
||||
rightSign);
|
||||
var resultSignChanged = _module.AddInstruction(
|
||||
SpirvOp.INotEqual,
|
||||
_boolType,
|
||||
leftSign,
|
||||
resultSign);
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
sameSourceSign,
|
||||
resultSignChanged);
|
||||
}
|
||||
|
||||
private uint SignedSubOverflow(uint left, uint right, uint result)
|
||||
{
|
||||
var leftSign = SignBit(left);
|
||||
var rightSign = SignBit(right);
|
||||
var resultSign = SignBit(result);
|
||||
var differentSourceSign = _module.AddInstruction(
|
||||
SpirvOp.INotEqual,
|
||||
_boolType,
|
||||
leftSign,
|
||||
rightSign);
|
||||
var resultSignChanged = _module.AddInstruction(
|
||||
SpirvOp.INotEqual,
|
||||
_boolType,
|
||||
leftSign,
|
||||
resultSign);
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.LogicalAnd,
|
||||
_boolType,
|
||||
differentSourceSign,
|
||||
resultSignChanged);
|
||||
}
|
||||
|
||||
private static bool TryDecodeInlineConstant(uint encoded, out uint value)
|
||||
{
|
||||
if (encoded == 125)
|
||||
|
||||
@@ -8,6 +8,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
private const uint ScalarRegisterCount = 256;
|
||||
private const uint VectorRegisterCount = 512;
|
||||
private const uint LdsDwordCount = 8192;
|
||||
private const uint RdnaWaveLaneCount = 32;
|
||||
|
||||
public static bool TryCompilePixelShader(
|
||||
Gen5ShaderState state,
|
||||
@@ -95,6 +96,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
private readonly List<uint> _interfaces = [];
|
||||
private readonly Dictionary<uint, uint> _pixelInputs = [];
|
||||
private readonly Dictionary<uint, uint> _vertexOutputs = [];
|
||||
private readonly Dictionary<uint, SpirvVertexInput> _vertexInputsByPc = [];
|
||||
private readonly List<SpirvImageResource> _imageResources = [];
|
||||
private readonly Dictionary<uint, int> _imageBindingByPc = [];
|
||||
private readonly Dictionary<uint, int> _bufferBindingByPc = [];
|
||||
@@ -150,6 +152,11 @@ internal static partial class Gen5SpirvTranslator
|
||||
ImageComponentKind ComponentKind,
|
||||
bool IsStorage);
|
||||
|
||||
private readonly record struct SpirvVertexInput(
|
||||
uint Variable,
|
||||
uint Type,
|
||||
uint ComponentCount);
|
||||
|
||||
public CompilationContext(
|
||||
Gen5SpirvStage stage,
|
||||
Gen5ShaderState state,
|
||||
@@ -283,7 +290,10 @@ internal static partial class Gen5SpirvTranslator
|
||||
_module.Build(),
|
||||
_evaluation.GlobalMemoryBindings,
|
||||
_evaluation.ImageBindings,
|
||||
attributeCount);
|
||||
attributeCount,
|
||||
_stage == Gen5SpirvStage.Vertex
|
||||
? _evaluation.VertexInputs ?? []
|
||||
: []);
|
||||
return true;
|
||||
}
|
||||
catch (Exception exception)
|
||||
@@ -298,6 +308,19 @@ internal static partial class Gen5SpirvTranslator
|
||||
_module.AddCapability(SpirvCapability.Shader);
|
||||
_module.AddCapability(SpirvCapability.Int64);
|
||||
_module.AddCapability(SpirvCapability.ImageQuery);
|
||||
if (_evaluation.ImageBindings.Any(
|
||||
static binding =>
|
||||
(binding.Opcode.StartsWith(
|
||||
"ImageSample",
|
||||
StringComparison.Ordinal) ||
|
||||
binding.Opcode.StartsWith(
|
||||
"ImageGather4",
|
||||
StringComparison.Ordinal)) &&
|
||||
binding.Opcode.EndsWith("O", StringComparison.Ordinal)))
|
||||
{
|
||||
_module.AddCapability(SpirvCapability.ImageGatherExtended);
|
||||
}
|
||||
|
||||
if (UsesSubgroupOperations())
|
||||
{
|
||||
_module.AddCapability(SpirvCapability.GroupNonUniform);
|
||||
@@ -389,10 +412,6 @@ internal static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
var ldsArrayType = _module.TypeArray(_uintType, LdsDwordCount);
|
||||
_module.AddDecoration(
|
||||
ldsArrayType,
|
||||
SpirvDecoration.ArrayStride,
|
||||
sizeof(uint));
|
||||
var ldsPointer =
|
||||
_module.TypePointer(SpirvStorageClass.Workgroup, ldsArrayType);
|
||||
_workgroupUintPointer =
|
||||
@@ -448,9 +467,6 @@ internal static partial class Gen5SpirvTranslator
|
||||
_imageBindingByPc.TryAdd(binding.Pc, index);
|
||||
var isStorage =
|
||||
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
|
||||
var isDepth =
|
||||
binding.Opcode.Contains("SampleC", StringComparison.Ordinal) ||
|
||||
binding.Opcode.Contains("Gather4C", StringComparison.Ordinal);
|
||||
var (format, componentKind) =
|
||||
DecodeImageFormat(binding.ResourceDescriptor);
|
||||
var componentType = componentKind switch
|
||||
@@ -466,11 +482,16 @@ internal static partial class Gen5SpirvTranslator
|
||||
_module.AddCapability(
|
||||
SpirvCapability.StorageImageWriteWithoutFormat);
|
||||
}
|
||||
else if (isStorage && RequiresExtendedStorageImageFormat(format))
|
||||
{
|
||||
_module.AddCapability(
|
||||
SpirvCapability.StorageImageExtendedFormats);
|
||||
}
|
||||
|
||||
var imageType = _module.TypeImage(
|
||||
componentType,
|
||||
SpirvImageDim.Dim2D,
|
||||
depth: isDepth,
|
||||
depth: false,
|
||||
arrayed: false,
|
||||
multisampled: false,
|
||||
sampled: isStorage ? 2u : 1u,
|
||||
@@ -503,6 +524,13 @@ internal static partial class Gen5SpirvTranslator
|
||||
}
|
||||
}
|
||||
|
||||
private static bool RequiresExtendedStorageImageFormat(
|
||||
SpirvImageFormat format) =>
|
||||
format is not SpirvImageFormat.Unknown and
|
||||
not SpirvImageFormat.Rgba32f and
|
||||
not SpirvImageFormat.Rgba32i and
|
||||
not SpirvImageFormat.Rgba32ui;
|
||||
|
||||
private static (SpirvImageFormat Format, ImageComponentKind Kind)
|
||||
DecodeImageFormat(IReadOnlyList<uint> descriptor)
|
||||
{
|
||||
@@ -583,6 +611,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
|
||||
if (_stage == Gen5SpirvStage.Vertex)
|
||||
{
|
||||
DeclareVertexInputs();
|
||||
|
||||
var inputPointer =
|
||||
_module.TypePointer(SpirvStorageClass.Input, _uintType);
|
||||
_vertexIndexInput = _module.AddGlobalVariable(
|
||||
@@ -628,7 +658,6 @@ internal static partial class Gen5SpirvTranslator
|
||||
outputPointer,
|
||||
SpirvStorageClass.Output);
|
||||
_module.AddDecoration(variable, SpirvDecoration.Location, parameter);
|
||||
_module.AddDecoration(variable, SpirvDecoration.NoPerspective);
|
||||
_vertexOutputs.Add(parameter, variable);
|
||||
_interfaces.Add(variable);
|
||||
}
|
||||
@@ -650,7 +679,6 @@ internal static partial class Gen5SpirvTranslator
|
||||
inputVec4Pointer,
|
||||
SpirvStorageClass.Input);
|
||||
_module.AddDecoration(variable, SpirvDecoration.Location, attribute);
|
||||
_module.AddDecoration(variable, SpirvDecoration.NoPerspective);
|
||||
_pixelInputs.Add(attribute, variable);
|
||||
_interfaces.Add(variable);
|
||||
}
|
||||
@@ -701,6 +729,42 @@ internal static partial class Gen5SpirvTranslator
|
||||
}
|
||||
}
|
||||
|
||||
private void DeclareVertexInputs()
|
||||
{
|
||||
foreach (var input in _evaluation.VertexInputs ?? [])
|
||||
{
|
||||
var type = input.ComponentCount switch
|
||||
{
|
||||
1u => _floatType,
|
||||
2u => _vec2Type,
|
||||
3u => _vec3Type,
|
||||
4u => _vec4Type,
|
||||
_ => 0u,
|
||||
};
|
||||
if (type == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var pointer = _module.TypePointer(SpirvStorageClass.Input, type);
|
||||
var variable = _module.AddGlobalVariable(
|
||||
pointer,
|
||||
SpirvStorageClass.Input);
|
||||
_module.AddName(variable, $"attr{input.Location}");
|
||||
_module.AddDecoration(
|
||||
variable,
|
||||
SpirvDecoration.Location,
|
||||
input.Location);
|
||||
_vertexInputsByPc.TryAdd(
|
||||
input.Pc,
|
||||
new SpirvVertexInput(
|
||||
variable,
|
||||
type,
|
||||
input.ComponentCount));
|
||||
_interfaces.Add(variable);
|
||||
}
|
||||
}
|
||||
|
||||
private void EmitInitialState()
|
||||
{
|
||||
for (uint index = 0;
|
||||
@@ -1287,6 +1351,19 @@ internal static partial class Gen5SpirvTranslator
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (_stage == Gen5SpirvStage.Vertex &&
|
||||
_vertexInputsByPc.TryGetValue(instruction.Pc, out var vertexInput))
|
||||
{
|
||||
return TryEmitVertexInputFetch(control, vertexInput, out error);
|
||||
}
|
||||
|
||||
if (_stage == Gen5SpirvStage.Vertex &&
|
||||
IsFormatBufferLoad(instruction.Opcode))
|
||||
{
|
||||
error = $"missing vertex input for {instruction.Opcode} pc=0x{instruction.Pc:X}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!_bufferBindingByPc.TryGetValue(instruction.Pc, out var bindingIndex))
|
||||
{
|
||||
error = "missing buffer-memory binding";
|
||||
@@ -1372,6 +1449,41 @@ internal static partial class Gen5SpirvTranslator
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool IsFormatBufferLoad(string opcode) =>
|
||||
opcode.StartsWith("BufferLoadFormat", StringComparison.Ordinal) ||
|
||||
opcode.StartsWith("TBufferLoadFormat", StringComparison.Ordinal);
|
||||
|
||||
private bool TryEmitVertexInputFetch(
|
||||
Gen5BufferMemoryControl control,
|
||||
SpirvVertexInput input,
|
||||
out string error)
|
||||
{
|
||||
error = string.Empty;
|
||||
if (control.DwordCount == 0 ||
|
||||
control.DwordCount > input.ComponentCount)
|
||||
{
|
||||
error =
|
||||
$"invalid vertex input fetch components={control.DwordCount} " +
|
||||
$"input={input.ComponentCount}";
|
||||
return false;
|
||||
}
|
||||
|
||||
var loaded = Load(input.Type, input.Variable);
|
||||
for (uint component = 0; component < control.DwordCount; component++)
|
||||
{
|
||||
var value = input.ComponentCount == 1
|
||||
? loaded
|
||||
: _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
_floatType,
|
||||
loaded,
|
||||
component);
|
||||
StoreV(control.VectorData + component, Bitcast(_uintType, value));
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryEmitImage(
|
||||
Gen5ShaderInstruction instruction,
|
||||
Gen5ImageControl image,
|
||||
@@ -1389,11 +1501,20 @@ internal static partial class Gen5SpirvTranslator
|
||||
var imageObject = Load(resource.ObjectType, resource.Variable);
|
||||
if (instruction.Opcode == "ImageGetResinfo")
|
||||
{
|
||||
var queryImage = resource.IsStorage
|
||||
? imageObject
|
||||
: _module.AddInstruction(
|
||||
SpirvOp.Image,
|
||||
resource.ImageType,
|
||||
imageObject);
|
||||
var size = _module.AddInstruction(
|
||||
SpirvOp.ImageQuerySizeLod,
|
||||
resource.IsStorage
|
||||
? SpirvOp.ImageQuerySize
|
||||
: SpirvOp.ImageQuerySizeLod,
|
||||
_module.TypeVector(_intType, 2),
|
||||
imageObject,
|
||||
UInt(0));
|
||||
resource.IsStorage
|
||||
? [queryImage]
|
||||
: [queryImage, UInt(0)]);
|
||||
uint outputIndex = 0;
|
||||
for (uint component = 0; component < 4; component++)
|
||||
{
|
||||
@@ -1504,10 +1625,14 @@ internal static partial class Gen5SpirvTranslator
|
||||
? BuildClampedIntegerCoordinates(image, 0, width, height)
|
||||
: BuildIntegerCoordinates(image, 0);
|
||||
var mipLevel = _evaluation.ImageBindings[bindingIndex].MipLevel ?? 0;
|
||||
var fetchedImage = _module.AddInstruction(
|
||||
SpirvOp.Image,
|
||||
resource.ImageType,
|
||||
imageObject);
|
||||
sampled = _module.AddInstruction(
|
||||
SpirvOp.ImageFetch,
|
||||
resource.VectorType,
|
||||
imageObject,
|
||||
fetchedImage,
|
||||
coordinates,
|
||||
2,
|
||||
UInt(mipLevel));
|
||||
@@ -1533,18 +1658,14 @@ internal static partial class Gen5SpirvTranslator
|
||||
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
||||
var imageOperands =
|
||||
(explicitLod ? 2u : 0u) | (hasOffset ? 0x10u : 0u);
|
||||
var reference = hasCompare
|
||||
? Bitcast(_floatType, LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))
|
||||
: 0u;
|
||||
var operands = new List<uint>
|
||||
{
|
||||
imageObject,
|
||||
coordinates,
|
||||
};
|
||||
if (hasCompare)
|
||||
{
|
||||
operands.Add(
|
||||
Bitcast(
|
||||
_floatType,
|
||||
LoadV(image.GetAddressRegister(hasOffset ? 1 : 0))));
|
||||
}
|
||||
|
||||
if (imageOperands != 0)
|
||||
{
|
||||
@@ -1561,29 +1682,14 @@ internal static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
sampled = _module.AddInstruction(
|
||||
hasCompare
|
||||
? explicitLod
|
||||
? SpirvOp.ImageSampleDrefExplicitLod
|
||||
: SpirvOp.ImageSampleDrefImplicitLod
|
||||
: explicitLod
|
||||
? SpirvOp.ImageSampleExplicitLod
|
||||
: SpirvOp.ImageSampleImplicitLod,
|
||||
hasCompare ? resource.ComponentType : resource.VectorType,
|
||||
explicitLod
|
||||
? SpirvOp.ImageSampleExplicitLod
|
||||
: SpirvOp.ImageSampleImplicitLod,
|
||||
resource.VectorType,
|
||||
[.. operands]);
|
||||
if (hasCompare)
|
||||
{
|
||||
var scalar = sampled;
|
||||
sampled = _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
resource.VectorType,
|
||||
scalar,
|
||||
scalar,
|
||||
scalar,
|
||||
resource.ComponentKind == ImageComponentKind.Float
|
||||
? Float(1)
|
||||
: resource.ComponentKind == ImageComponentKind.Uint
|
||||
? UInt(1)
|
||||
: _module.Constant(_intType, 1));
|
||||
sampled = EmitManualDepthCompare(resource, sampled, reference);
|
||||
}
|
||||
}
|
||||
else if (instruction.Opcode.StartsWith(
|
||||
@@ -1597,6 +1703,9 @@ internal static partial class Gen5SpirvTranslator
|
||||
var start = (hasOffset ? 1 : 0) + (hasCompare ? 1 : 0);
|
||||
var coordinates = BuildFloatCoordinates(image, start);
|
||||
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
||||
var reference = hasCompare
|
||||
? Bitcast(_floatType, LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))
|
||||
: 0u;
|
||||
var operands = new List<uint>
|
||||
{
|
||||
imageObject,
|
||||
@@ -1604,10 +1713,7 @@ internal static partial class Gen5SpirvTranslator
|
||||
};
|
||||
if (hasCompare)
|
||||
{
|
||||
operands.Add(
|
||||
Bitcast(
|
||||
_floatType,
|
||||
LoadV(image.GetAddressRegister(hasOffset ? 1 : 0))));
|
||||
operands.Add(UInt(0));
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -1628,11 +1734,28 @@ internal static partial class Gen5SpirvTranslator
|
||||
}
|
||||
|
||||
sampled = _module.AddInstruction(
|
||||
hasCompare
|
||||
? SpirvOp.ImageDrefGather
|
||||
: SpirvOp.ImageGather,
|
||||
SpirvOp.ImageGather,
|
||||
resource.VectorType,
|
||||
[.. operands]);
|
||||
if (hasCompare)
|
||||
{
|
||||
var compared = new uint[4];
|
||||
for (var component = 0u; component < 4; component++)
|
||||
{
|
||||
var texel = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
resource.ComponentType,
|
||||
sampled,
|
||||
component);
|
||||
compared[component] = EmitDepthCompareScalar(resource, texel, reference);
|
||||
}
|
||||
|
||||
sampled = _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
resource.VectorType,
|
||||
compared);
|
||||
}
|
||||
|
||||
writeAllComponents = true;
|
||||
}
|
||||
else
|
||||
@@ -1666,6 +1789,67 @@ internal static partial class Gen5SpirvTranslator
|
||||
return true;
|
||||
}
|
||||
|
||||
private uint EmitDepthCompareScalar(
|
||||
SpirvImageResource resource,
|
||||
uint texel,
|
||||
uint reference)
|
||||
{
|
||||
var texelAsFloat = resource.ComponentKind switch
|
||||
{
|
||||
ImageComponentKind.Uint => _module.AddInstruction(
|
||||
SpirvOp.ConvertUToF, _floatType, texel),
|
||||
ImageComponentKind.Sint => _module.AddInstruction(
|
||||
SpirvOp.ConvertSToF, _floatType, texel),
|
||||
_ => texel,
|
||||
};
|
||||
var passes = _module.AddInstruction(
|
||||
SpirvOp.FOrdLessThanEqual,
|
||||
_boolType,
|
||||
reference,
|
||||
texelAsFloat);
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
resource.ComponentType,
|
||||
passes,
|
||||
resource.ComponentKind switch
|
||||
{
|
||||
ImageComponentKind.Uint => UInt(1),
|
||||
ImageComponentKind.Sint => _module.Constant(_intType, 1),
|
||||
_ => Float(1),
|
||||
},
|
||||
resource.ComponentKind switch
|
||||
{
|
||||
ImageComponentKind.Uint => UInt(0),
|
||||
ImageComponentKind.Sint => _module.Constant(_intType, 0),
|
||||
_ => Float(0),
|
||||
});
|
||||
}
|
||||
|
||||
private uint EmitManualDepthCompare(
|
||||
SpirvImageResource resource,
|
||||
uint sampledVector,
|
||||
uint reference)
|
||||
{
|
||||
var texel = _module.AddInstruction(
|
||||
SpirvOp.CompositeExtract,
|
||||
resource.ComponentType,
|
||||
sampledVector,
|
||||
0u);
|
||||
var scalar = EmitDepthCompareScalar(resource, texel, reference);
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
resource.VectorType,
|
||||
scalar,
|
||||
scalar,
|
||||
scalar,
|
||||
resource.ComponentKind switch
|
||||
{
|
||||
ImageComponentKind.Uint => UInt(1),
|
||||
ImageComponentKind.Sint => _module.Constant(_intType, 1),
|
||||
_ => Float(1),
|
||||
});
|
||||
}
|
||||
|
||||
private uint BuildFloatCoordinates(Gen5ImageControl image, int start)
|
||||
{
|
||||
var x = Bitcast(
|
||||
@@ -2057,18 +2241,13 @@ internal static partial class Gen5SpirvTranslator
|
||||
private void StoreS(uint register, uint value)
|
||||
{
|
||||
Store(ScalarPointer(register), value);
|
||||
if (_subgroupInvocationIdInput == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (register is 106 or 107)
|
||||
{
|
||||
Store(_vcc, IsCurrentLaneSet(LoadS64(106)));
|
||||
Store(_vcc, IsWaveMaskActive(LoadS64(106)));
|
||||
}
|
||||
else if (register is 126 or 127)
|
||||
{
|
||||
Store(_exec, IsCurrentLaneSet(LoadS64(126)));
|
||||
Store(_exec, IsWaveMaskActive(LoadS64(126)));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2105,12 +2284,49 @@ internal static partial class Gen5SpirvTranslator
|
||||
private uint IAdd(uint left, uint right) =>
|
||||
_module.AddInstruction(SpirvOp.IAdd, _uintType, left, right);
|
||||
|
||||
private uint ShiftLeftLogical(uint left, uint right) =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
_uintType,
|
||||
left,
|
||||
BitwiseAnd(right, UInt(31)));
|
||||
|
||||
private uint ShiftRightLogical(uint left, uint right) =>
|
||||
_module.AddInstruction(SpirvOp.ShiftRightLogical, _uintType, left, right);
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ShiftRightLogical,
|
||||
_uintType,
|
||||
left,
|
||||
BitwiseAnd(right, UInt(31)));
|
||||
|
||||
private uint ShiftRightArithmetic(uint left, uint right) =>
|
||||
Bitcast(
|
||||
_uintType,
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ShiftRightArithmetic,
|
||||
_intType,
|
||||
Bitcast(_intType, left),
|
||||
BitwiseAnd(right, UInt(31))));
|
||||
|
||||
private uint ShiftLeftLogical64(uint left, uint right) =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
_ulongType,
|
||||
left,
|
||||
BitwiseAnd64(right, _module.Constant64(_ulongType, 63)));
|
||||
|
||||
private uint ShiftRightLogical64(uint left, uint right) =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ShiftRightLogical,
|
||||
_ulongType,
|
||||
left,
|
||||
BitwiseAnd64(right, _module.Constant64(_ulongType, 63)));
|
||||
|
||||
private uint BitwiseAnd(uint left, uint right) =>
|
||||
_module.AddInstruction(SpirvOp.BitwiseAnd, _uintType, left, right);
|
||||
|
||||
private uint BitwiseAnd64(uint left, uint right) =>
|
||||
_module.AddInstruction(SpirvOp.BitwiseAnd, _ulongType, left, right);
|
||||
|
||||
private uint BitwiseOr(uint left, uint right) =>
|
||||
_module.AddInstruction(SpirvOp.BitwiseOr, _uintType, left, right);
|
||||
|
||||
@@ -2121,25 +2337,44 @@ internal static partial class Gen5SpirvTranslator
|
||||
_module.AddInstruction(SpirvOp.LogicalNot, _boolType, value);
|
||||
|
||||
private uint SubgroupAny(uint condition) =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.GroupNonUniformAny,
|
||||
_boolType,
|
||||
UInt(3),
|
||||
condition);
|
||||
_subgroupInvocationIdInput == 0
|
||||
? condition
|
||||
: _module.AddInstruction(
|
||||
SpirvOp.GroupNonUniformAny,
|
||||
_boolType,
|
||||
UInt(3),
|
||||
condition);
|
||||
|
||||
private uint CurrentLaneBit()
|
||||
{
|
||||
var lane = _module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_ulongType,
|
||||
Load(_uintType, _subgroupInvocationIdInput));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.ShiftLeftLogical,
|
||||
_ulongType,
|
||||
if (_subgroupInvocationIdInput == 0)
|
||||
{
|
||||
return _module.Constant64(_ulongType, 1);
|
||||
}
|
||||
|
||||
var lane = Load(_uintType, _subgroupInvocationIdInput);
|
||||
var maskedLane = BitwiseAnd(lane, UInt(RdnaWaveLaneCount - 1));
|
||||
var shifted = ShiftLeftLogical64(
|
||||
_module.Constant64(_ulongType, 1),
|
||||
lane);
|
||||
_module.AddInstruction(
|
||||
SpirvOp.UConvert,
|
||||
_ulongType,
|
||||
maskedLane));
|
||||
return _module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
_ulongType,
|
||||
IsCurrentLaneInRdnaWave(),
|
||||
shifted,
|
||||
_module.Constant64(_ulongType, 0));
|
||||
}
|
||||
|
||||
private uint IsCurrentLaneInRdnaWave() =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.ULessThan,
|
||||
_boolType,
|
||||
Load(_uintType, _subgroupInvocationIdInput),
|
||||
UInt(RdnaWaveLaneCount));
|
||||
|
||||
private uint BooleanToLaneMask(uint condition) =>
|
||||
_module.AddInstruction(
|
||||
SpirvOp.Select,
|
||||
@@ -2148,6 +2383,11 @@ internal static partial class Gen5SpirvTranslator
|
||||
CurrentLaneBit(),
|
||||
_module.Constant64(_ulongType, 0));
|
||||
|
||||
private uint IsWaveMaskActive(uint mask) =>
|
||||
_subgroupInvocationIdInput == 0
|
||||
? IsNotZero64(mask)
|
||||
: IsCurrentLaneSet(mask);
|
||||
|
||||
private uint IsCurrentLaneSet(uint mask) =>
|
||||
IsNotZero64(
|
||||
_module.AddInstruction(
|
||||
@@ -2163,10 +2403,11 @@ internal static partial class Gen5SpirvTranslator
|
||||
{
|
||||
var activeLabel = _module.AllocateId();
|
||||
var mergeLabel = _module.AllocateId();
|
||||
var active = Load(_boolType, _exec);
|
||||
_module.AddStatement(SpirvOp.SelectionMerge, mergeLabel, 0);
|
||||
_module.AddStatement(
|
||||
SpirvOp.BranchConditional,
|
||||
Load(_boolType, _exec),
|
||||
active,
|
||||
activeLabel,
|
||||
mergeLabel);
|
||||
_module.AddLabel(activeLabel);
|
||||
@@ -2193,7 +2434,8 @@ internal static partial class Gen5SpirvTranslator
|
||||
instruction.Destinations.Any(IsWaveMaskOperand));
|
||||
|
||||
private bool UsesSubgroupOperations() =>
|
||||
UsesSubgroupShuffle() || UsesWaveControl();
|
||||
_stage == Gen5SpirvStage.Compute &&
|
||||
(UsesSubgroupShuffle() || UsesWaveControl());
|
||||
|
||||
private static bool IsWaveMaskOperand(Gen5Operand operand) =>
|
||||
operand.Kind == Gen5OperandKind.ScalarRegister &&
|
||||
|
||||
@@ -62,6 +62,7 @@ internal enum SpirvOp : ushort
|
||||
ImageDrefGather = 97,
|
||||
ImageRead = 98,
|
||||
ImageWrite = 99,
|
||||
Image = 100,
|
||||
ImageQuerySizeLod = 103,
|
||||
ImageQuerySize = 104,
|
||||
ImageQueryLod = 105,
|
||||
@@ -173,6 +174,8 @@ internal enum SpirvCapability : uint
|
||||
Float64 = 10,
|
||||
Int64 = 11,
|
||||
Int16 = 22,
|
||||
ImageGatherExtended = 25,
|
||||
StorageImageExtendedFormats = 49,
|
||||
ImageQuery = 50,
|
||||
StorageImageReadWithoutFormat = 55,
|
||||
StorageImageWriteWithoutFormat = 56,
|
||||
@@ -310,6 +313,18 @@ internal sealed class SpirvModuleBuilder
|
||||
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 = [];
|
||||
@@ -478,6 +493,19 @@ internal sealed class SpirvModuleBuilder
|
||||
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,
|
||||
@@ -490,13 +518,20 @@ internal sealed class SpirvModuleBuilder
|
||||
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;
|
||||
}
|
||||
|
||||
|
||||
@@ -24,6 +24,7 @@ public static class AmprExports
|
||||
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 =
|
||||
@@ -37,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",
|
||||
@@ -249,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;
|
||||
@@ -435,7 +453,7 @@ public static class AmprExports
|
||||
var offset = 0UL;
|
||||
while (offset < writeOffset)
|
||||
{
|
||||
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
|
||||
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -634,24 +652,43 @@ public static class AmprExports
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(
|
||||
hostPath,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite | FileShare.Delete,
|
||||
ChunkSize,
|
||||
FileOptions.SequentialScan);
|
||||
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;
|
||||
@@ -681,6 +718,44 @@ public static class AmprExports
|
||||
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,
|
||||
@@ -808,19 +883,6 @@ public static class AmprExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
||||
{
|
||||
if (!_traceAmpr)
|
||||
|
||||
@@ -73,7 +73,7 @@ public static class AppContentExports
|
||||
var valueAddress = ctx[CpuRegister.Rsi];
|
||||
if (valueAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
int value;
|
||||
@@ -83,18 +83,18 @@ public static class AppContentExports
|
||||
}
|
||||
else if (!TryReadUserDefinedParam(paramId, out value))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceAppContent($"app_param_get_int id={paramId} value={value}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -168,12 +168,6 @@ public static class AppContentExports
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
private static void TraceAppContent(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
@@ -37,7 +36,7 @@ public static class AudioOut2Exports
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||
@@ -48,8 +47,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
||||
|
||||
return ctx.Memory.TryWrite(paramAddress, param)
|
||||
? 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(
|
||||
@@ -63,7 +62,7 @@ public static class AudioOut2Exports
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> memoryInfo = stackalloc byte[0x20];
|
||||
@@ -74,8 +73,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
||||
|
||||
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
||||
? 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(
|
||||
@@ -91,13 +90,13 @@ public static class AudioOut2Exports
|
||||
var outContextAddress = ctx[CpuRegister.Rcx];
|
||||
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||
return TryWriteUInt64(ctx, outContextAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -105,7 +104,7 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2ContextDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JK2wamZPzwM",
|
||||
@@ -120,14 +119,14 @@ public static class AudioOut2Exports
|
||||
var contextAddress = ctx[CpuRegister.Rcx];
|
||||
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
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 TryWriteUInt64(ctx, outPortAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.TryWriteUInt64(outPortAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -141,7 +140,7 @@ public static class AudioOut2Exports
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle == 0 || stateAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var type = (int)((handle >> 16) & 0xFF);
|
||||
@@ -154,8 +153,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||
|
||||
return ctx.Memory.TryWrite(stateAddress, state)
|
||||
? 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(
|
||||
@@ -168,7 +167,7 @@ public static class AudioOut2Exports
|
||||
var infoAddress = ctx[CpuRegister.Rdi];
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> info = stackalloc byte[0x40];
|
||||
@@ -178,8 +177,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
||||
|
||||
return ctx.Memory.TryWrite(infoAddress, info)
|
||||
? 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(
|
||||
@@ -187,14 +186,14 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2PortDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
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) => SetReturn(ctx, 0);
|
||||
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xywYcRB7nbQ",
|
||||
@@ -207,25 +206,12 @@ public static class AudioOut2Exports
|
||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||
return TryWriteUInt64(ctx, outUserAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
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,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;
|
||||
}
|
||||
}
|
||||
@@ -104,17 +104,17 @@ public static class FiberExports
|
||||
var optParam = ctx[CpuRegister.Rdi];
|
||||
if (optParam == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((optParam & 7) != 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorAlignment);
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
}
|
||||
|
||||
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -127,24 +127,24 @@ public static class FiberExports
|
||||
var fiber = ctx[CpuRegister.Rdi];
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return SetReturn(ctx, error);
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
|
||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (state != FiberStateIdle)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations.TryRemove(fiber, out _);
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
_stackRanges.TryRemove(fiber, out _);
|
||||
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
|
||||
return SetReturn(ctx, 0);
|
||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -229,20 +229,20 @@ public static class FiberExports
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
var returnArgument = ctx[CpuRegister.Rdi];
|
||||
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
||||
if (argOnRunAddress != 0 && !TryWriteUInt64(ctx, argOnRunAddress, 0))
|
||||
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
@@ -256,7 +256,7 @@ public static class FiberExports
|
||||
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
previousFiber = returnTarget.PreviousFiber;
|
||||
@@ -264,10 +264,10 @@ public static class FiberExports
|
||||
{
|
||||
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
||||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
||||
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun))
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = previousContinuation.Context with { Rax = 0 };
|
||||
@@ -278,15 +278,15 @@ public static class FiberExports
|
||||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
||||
}
|
||||
|
||||
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ public static class FiberExports
|
||||
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 SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -309,18 +309,18 @@ public static class FiberExports
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
return TryWriteUInt64(ctx, outAddress, fiberAddress)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -334,35 +334,35 @@ public static class FiberExports
|
||||
var info = ctx[CpuRegister.Rsi];
|
||||
if (info == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return SetReturn(ctx, error);
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (!TryReadUInt64(ctx, info, out var size) || size != FiberInfoSize)
|
||||
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
|
||||
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
|
||||
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
|
||||
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
|
||||
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) ||
|
||||
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
|
||||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -376,22 +376,22 @@ public static class FiberExports
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return SetReturn(ctx, error);
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (nameAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -403,12 +403,12 @@ public static class FiberExports
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] != 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorState);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -419,8 +419,8 @@ public static class FiberExports
|
||||
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
||||
{
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorState);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -433,17 +433,17 @@ public static class FiberExports
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if (ResolveCurrentFiberAddress(ctx) == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
return TryWriteUInt64(ctx, outAddress, ctx[CpuRegister.Rbp])
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
private static int FiberInitializeCore(
|
||||
@@ -459,37 +459,37 @@ public static class FiberExports
|
||||
{
|
||||
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((fiber & 7) != 0 ||
|
||||
(contextAddress & 15) != 0 ||
|
||||
(optParam & 7) != 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorAlignment);
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
}
|
||||
|
||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorRange);
|
||||
return ctx.SetReturn(FiberErrorRange);
|
||||
}
|
||||
|
||||
if ((contextSize & 15) != 0 ||
|
||||
(contextAddress == 0 && contextSize != 0) ||
|
||||
(contextAddress != 0 && contextSize == 0))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (optParam != 0 &&
|
||||
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
||||
@@ -497,27 +497,27 @@ public static class FiberExports
|
||||
flags |= FiberFlagContextSizeCheck;
|
||||
}
|
||||
|
||||
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||
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) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
|
||||
!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 SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (contextAddress != 0)
|
||||
{
|
||||
if (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if ((flags & FiberFlagContextSizeCheck) != 0)
|
||||
@@ -532,7 +532,7 @@ public static class FiberExports
|
||||
}
|
||||
|
||||
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static int FiberRunCore(
|
||||
@@ -547,12 +547,12 @@ public static class FiberExports
|
||||
{
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return SetReturn(ctx, error);
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (attachContextAddress != 0 || attachContextSize != 0)
|
||||
@@ -560,7 +560,7 @@ public static class FiberExports
|
||||
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
||||
if (attachResult != 0)
|
||||
{
|
||||
return SetReturn(ctx, attachResult);
|
||||
return ctx.SetReturn(attachResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,16 +568,16 @@ public static class FiberExports
|
||||
if ((isSwitch && previousFiber == 0) ||
|
||||
(!isSwitch && previousFiber != 0))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
if (previousFiber == fiber)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
@@ -586,12 +586,12 @@ public static class FiberExports
|
||||
{
|
||||
if (!TryReadFiberFields(ctx, fiber, out fields))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
if (fields.State != FiberStateIdle)
|
||||
{
|
||||
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
FiberContinuation targetContinuation;
|
||||
@@ -602,12 +602,12 @@ public static class FiberExports
|
||||
}
|
||||
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
var callerContinuation = new FiberContinuation(
|
||||
@@ -616,11 +616,11 @@ public static class FiberExports
|
||||
|
||||
if (previousFiber != 0)
|
||||
{
|
||||
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
|
||||
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
|
||||
previousState != FiberStateRun ||
|
||||
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations[previousFiber] = callerContinuation;
|
||||
@@ -631,15 +631,15 @@ public static class FiberExports
|
||||
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
||||
}
|
||||
|
||||
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
|
||||
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
|
||||
{
|
||||
if (previousFiber != 0)
|
||||
{
|
||||
_continuations.TryRemove(previousFiber, out _);
|
||||
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
|
||||
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
||||
}
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed)
|
||||
@@ -656,7 +656,7 @@ public static class FiberExports
|
||||
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 SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryCreateInitialContinuation(
|
||||
@@ -673,7 +673,7 @@ public static class FiberExports
|
||||
|
||||
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
||||
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
||||
if (!TryWriteUInt64(ctx, entryRsp, 0))
|
||||
if (!ctx.TryWriteUInt64(entryRsp, 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -708,7 +708,7 @@ public static class FiberExports
|
||||
FiberContinuation continuation,
|
||||
ulong argument) =>
|
||||
continuation.ArgOnRunAddress == 0 ||
|
||||
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
|
||||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
|
||||
|
||||
private static GuestCpuContinuation CaptureContinuation(
|
||||
CpuContext ctx,
|
||||
@@ -779,11 +779,11 @@ public static class FiberExports
|
||||
return FiberErrorInvalid;
|
||||
}
|
||||
|
||||
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||
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;
|
||||
}
|
||||
@@ -839,8 +839,8 @@ public static class FiberExports
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||
magicStart != FiberSignature0 ||
|
||||
magicEnd != FiberSignature1)
|
||||
{
|
||||
@@ -855,12 +855,12 @@ public static class FiberExports
|
||||
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
||||
{
|
||||
fields = default;
|
||||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
|
||||
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;
|
||||
@@ -898,46 +898,6 @@ public static class FiberExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
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 TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
||||
@@ -965,37 +925,6 @@ public static class FiberExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||
{
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||
{
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes[index] = current[0];
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceFiber(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Ampr;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
@@ -50,7 +49,7 @@ public static class KernelAprCompatExports
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||
if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -141,7 +140,7 @@ public static class KernelAprCompatExports
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||
if (!ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -178,13 +177,6 @@ public static class KernelAprCompatExports
|
||||
return tag is 0x0C or 0x10;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static void TraceApr(
|
||||
CpuContext ctx,
|
||||
string operation,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 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;
|
||||
@@ -52,17 +51,17 @@ public static class KernelEventFlagCompatExports
|
||||
optionAddress != 0 ||
|
||||
!IsValidAttributes(attributes))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
||||
@@ -76,11 +75,11 @@ public static class KernelEventFlagCompatExports
|
||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||
{
|
||||
_eventFlags.TryRemove(handle, out _);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -93,7 +92,7 @@ public static class KernelEventFlagCompatExports
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
if (!_eventFlags.TryRemove(handle, out var state))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
@@ -102,7 +101,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -117,7 +116,7 @@ public static class KernelEventFlagCompatExports
|
||||
var returnRip = GetCurrentReturnRip();
|
||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
@@ -128,7 +127,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -142,7 +141,7 @@ public static class KernelEventFlagCompatExports
|
||||
var pattern = ctx[CpuRegister.Rsi];
|
||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
@@ -151,7 +150,7 @@ public static class KernelEventFlagCompatExports
|
||||
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -168,29 +167,29 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,18 +209,18 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
uint timeoutUsec = 0;
|
||||
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Monitor.Enter(state.Gate);
|
||||
@@ -229,15 +228,15 @@ public static class KernelEventFlagCompatExports
|
||||
{
|
||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
||||
{
|
||||
return SetReturn(ctx, immediateWaitResult);
|
||||
return ctx.SetReturn(immediateWaitResult);
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt32(ctx, 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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
@@ -272,7 +271,7 @@ public static class KernelEventFlagCompatExports
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (scheduler is null)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
state.WaitingThreads++;
|
||||
@@ -297,7 +296,7 @@ public static class KernelEventFlagCompatExports
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
releaseWaiter = false;
|
||||
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, pumpedWaitResult);
|
||||
return ctx.SetReturn(pumpedWaitResult);
|
||||
}
|
||||
|
||||
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
||||
@@ -314,7 +313,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
state.WaitingThreads++;
|
||||
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -334,15 +333,15 @@ public static class KernelEventFlagCompatExports
|
||||
var waiterCountAddress = ctx[CpuRegister.Rdx];
|
||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (waiterCountAddress != 0 &&
|
||||
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
state.Bits = setPattern;
|
||||
@@ -351,7 +350,7 @@ public static class KernelEventFlagCompatExports
|
||||
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
|
||||
}
|
||||
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static bool IsValidAttributes(uint attributes)
|
||||
@@ -453,84 +452,6 @@ public static class KernelEventFlagCompatExports
|
||||
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
||||
address == 0 || ctx.TryWriteUInt64(address, bits);
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = buffer[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||
{
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||
{
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes[index] = current[0];
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
var value = (int)result;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void TraceEventFlag(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
|
||||
@@ -614,7 +535,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
if (TryReadByte(ctx, address, out var value))
|
||||
if (ctx.TryReadByte(address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X2}");
|
||||
}
|
||||
@@ -622,7 +543,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
if (TryReadUInt32(ctx, address, out var value))
|
||||
if (ctx.TryReadUInt32(address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X8}");
|
||||
}
|
||||
@@ -630,7 +551,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
if (TryReadUInt64(ctx, address, out var value))
|
||||
if (ctx.TryReadUInt64(address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X16}");
|
||||
}
|
||||
|
||||
@@ -276,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;
|
||||
}
|
||||
@@ -300,6 +316,41 @@ public static class KernelEventQueueCompatExports
|
||||
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;
|
||||
}
|
||||
@@ -330,6 +381,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
queue.AddLast(queuedEvent);
|
||||
queued = true;
|
||||
Monitor.PulseAll(_eventQueueGate);
|
||||
}
|
||||
|
||||
if (queued)
|
||||
@@ -489,12 +541,14 @@ public static class KernelEventQueueCompatExports
|
||||
ulong outCountAddress)
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
return deliveredCount > 0
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
}
|
||||
|
||||
private static bool HasPendingEvents(ulong handle)
|
||||
@@ -600,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();
|
||||
@@ -53,14 +52,7 @@ public static class KernelExports
|
||||
ExportName = "exit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Exit(CpuContext ctx)
|
||||
{
|
||||
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
|
||||
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "XKRegsFpEpk",
|
||||
@@ -173,11 +165,7 @@ public static class KernelExports
|
||||
ExportName = "_exit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int UnderscoreExit(CpuContext ctx)
|
||||
{
|
||||
_ = ctx;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Ac86z8q7T8A",
|
||||
@@ -306,10 +294,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())
|
||||
{
|
||||
@@ -317,6 +301,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;
|
||||
}
|
||||
@@ -345,6 +353,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",
|
||||
@@ -411,4 +426,13 @@ public static class KernelExports
|
||||
{
|
||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static int RequestProcessExit(CpuContext ctx, string syscallName)
|
||||
{
|
||||
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
|
||||
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,11 +5,9 @@ using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Ampr;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Reflection;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -99,12 +97,16 @@ public static class KernelMemoryCompatExports
|
||||
private static readonly object _tlsGate = new();
|
||||
private static readonly object _ioTraceGate = new();
|
||||
private static readonly object _statCacheGate = new();
|
||||
private static readonly object _guestMountGate = new();
|
||||
private static readonly Dictionary<ulong, DirectAllocation> _directAllocations = new();
|
||||
private static readonly Dictionary<ulong, LibcHeapAllocation> _libcAllocations = new();
|
||||
private static readonly Dictionary<ulong, MappedRegion> _mappedRegions = new();
|
||||
private static readonly Dictionary<ulong, string> _mappedRegionNames = new();
|
||||
private static readonly Dictionary<ulong, ulong> _tlsModuleBlocks = new();
|
||||
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
||||
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly ConcurrentDictionary<string, ulong> _aprFileSizeCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static long _nextFileDescriptor = 2;
|
||||
private static ulong _nextPhysicalAddress;
|
||||
private static ulong _nextVirtualAddress;
|
||||
@@ -153,6 +155,32 @@ public static class KernelMemoryCompatExports
|
||||
private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, bool IsDirect, ulong DirectStart);
|
||||
private readonly record struct BatchMapEntry(ulong Start, ulong Offset, ulong Length, byte Protection, byte Type, int Operation);
|
||||
|
||||
public static void RegisterGuestPathMount(string guestMountPoint, string hostRoot)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(guestMountPoint);
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(hostRoot);
|
||||
|
||||
var normalizedMountPoint = NormalizeGuestStatCachePath(guestMountPoint);
|
||||
if (normalizedMountPoint is null || normalizedMountPoint == "/")
|
||||
{
|
||||
throw new ArgumentException("Guest mount point must name a directory.", nameof(guestMountPoint));
|
||||
}
|
||||
|
||||
var normalizedHostRoot = Path.GetFullPath(hostRoot);
|
||||
Directory.CreateDirectory(normalizedHostRoot);
|
||||
lock (_guestMountGate)
|
||||
{
|
||||
_guestMounts[normalizedMountPoint] = normalizedHostRoot;
|
||||
}
|
||||
|
||||
lock (_statCacheGate)
|
||||
{
|
||||
_negativeStatCache.RemoveWhere(path =>
|
||||
string.Equals(path, normalizedMountPoint, StringComparison.OrdinalIgnoreCase) ||
|
||||
path.StartsWith(normalizedMountPoint + "/", StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryAllocateHleData(
|
||||
CpuContext ctx,
|
||||
ulong length,
|
||||
@@ -203,6 +231,25 @@ public static class KernelMemoryCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
internal static void RegisterReservedVirtualRange(ulong address, ulong length)
|
||||
{
|
||||
if (address == 0 || length == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_memoryGate)
|
||||
{
|
||||
_mappedRegions[address] = new MappedRegion(
|
||||
address,
|
||||
length,
|
||||
Protection: 0,
|
||||
IsFlexible: false,
|
||||
IsDirect: false,
|
||||
DirectStart: 0);
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8zTFvBIAIN8",
|
||||
ExportName = "memset",
|
||||
@@ -1294,6 +1341,7 @@ public static class KernelMemoryCompatExports
|
||||
if (IsMutatingOpen(flags))
|
||||
{
|
||||
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
||||
InvalidateAprFileSizeCache(hostPath);
|
||||
}
|
||||
|
||||
LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}");
|
||||
@@ -1502,6 +1550,7 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
File.Delete(hostPath);
|
||||
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
||||
InvalidateAprFileSizeCache(hostPath);
|
||||
AddNegativeStatCacheForGuestPath(guestPath);
|
||||
LogOpenTrace($"unlink path='{guestPath}' host='{hostPath}'");
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -2407,6 +2456,36 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "hwVSPCmp5tM",
|
||||
ExportName = "sceKernelCheckedReleaseDirectMemory",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelCheckedReleaseDirectMemory(CpuContext ctx)
|
||||
{
|
||||
var start = ctx[CpuRegister.Rdi];
|
||||
var length = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (length == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
lock (_memoryGate)
|
||||
{
|
||||
if (!_directAllocations.TryGetValue(start, out var allocation) ||
|
||||
allocation.Length != length)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
_directAllocations.Remove(start);
|
||||
_nextPhysicalAddress = GetDirectMemoryHighWaterMarkLocked();
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "L-Q3LEjIbgA",
|
||||
ExportName = "sceKernelMapDirectMemory",
|
||||
@@ -2450,6 +2529,17 @@ public static class KernelMemoryCompatExports
|
||||
if (fixedMapping && requestedAddress != 0)
|
||||
{
|
||||
mappedAddress = requestedAddress;
|
||||
reserved = IsGuestRangeBacked(ctx, requestedAddress, length);
|
||||
if (!reserved)
|
||||
{
|
||||
TryReserveExactGuestVirtualRange(ctx, requestedAddress, length, protection);
|
||||
reserved = IsGuestRangeBacked(ctx, requestedAddress, length);
|
||||
}
|
||||
|
||||
if (!reserved)
|
||||
{
|
||||
mappedAddress = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2460,7 +2550,7 @@ public static class KernelMemoryCompatExports
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] map_direct reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} reserved={reserved} mapped=0x{mappedAddress:X16}");
|
||||
}
|
||||
if (!reserved)
|
||||
if (!reserved && !fixedMapping)
|
||||
{
|
||||
if (mappedAddress == 0)
|
||||
{
|
||||
@@ -2518,8 +2608,11 @@ public static class KernelMemoryCompatExports
|
||||
var length = ctx[CpuRegister.Rsi];
|
||||
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
var flags = ctx[CpuRegister.Rcx];
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] map_flexible: inout=0x{inOutAddressPointer:X16} len=0x{length:X16} prot=0x{protection:X8} flags=0x{flags:X16}");
|
||||
if (ShouldTraceDirectMemory())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] map_flexible: inout=0x{inOutAddressPointer:X16} len=0x{length:X16} prot=0x{protection:X8} flags=0x{flags:X16}");
|
||||
}
|
||||
if (inOutAddressPointer == 0 || length == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
@@ -2541,16 +2634,25 @@ public static class KernelMemoryCompatExports
|
||||
if (fixedMapping && requestedAddress != 0)
|
||||
{
|
||||
mappedAddress = requestedAddress;
|
||||
if (!IsGuestRangeBacked(ctx, requestedAddress, length))
|
||||
{
|
||||
TryReserveExactGuestVirtualRange(ctx, requestedAddress, length, protection);
|
||||
if (!IsGuestRangeBacked(ctx, requestedAddress, length))
|
||||
{
|
||||
mappedAddress = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!TryReserveGuestVirtualRange(ctx, desiredAddress, length, protection, OrbisPageSize, out mappedAddress))
|
||||
{
|
||||
mappedAddress = requestedAddress != 0 && fixedMapping
|
||||
? requestedAddress
|
||||
: AllocateMappedGuestAddress(ctx, length, 0x1000UL);
|
||||
mappedAddress = AllocateMappedGuestAddress(ctx, length, 0x1000UL);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] map_flexible reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16}");
|
||||
if (ShouldTraceDirectMemory())
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] map_flexible reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16}");
|
||||
}
|
||||
|
||||
if (mappedAddress == 0)
|
||||
{
|
||||
@@ -2586,6 +2688,16 @@ public static class KernelMemoryCompatExports
|
||||
return KernelMapNamedFlexibleMemory(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "4h6F1LLbTiw",
|
||||
ExportName = "sceKernelMapFlexibleMemoryInternal",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelMapFlexibleMemoryInternal(CpuContext ctx)
|
||||
{
|
||||
return KernelMapNamedFlexibleMemory(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "2SKEx6bSq-4",
|
||||
ExportName = "sceKernelBatchMap",
|
||||
@@ -2639,6 +2751,44 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "DGMG3JshrZU",
|
||||
ExportName = "sceKernelSetVirtualRangeName",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelSetVirtualRangeName(CpuContext ctx)
|
||||
{
|
||||
var address = ctx[CpuRegister.Rdi];
|
||||
var length = ctx[CpuRegister.Rsi];
|
||||
var nameAddress = ctx[CpuRegister.Rdx];
|
||||
if (nameAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (!TryReadCString(ctx, nameAddress, OrbisKernelMaximumNameLength, out var nameBytes))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var name = Encoding.UTF8.GetString(nameBytes);
|
||||
lock (_memoryGate)
|
||||
{
|
||||
if (!TryFindVirtualQueryRegionLocked(address, findNext: false, out var region) ||
|
||||
length > region.Length ||
|
||||
address < region.Address ||
|
||||
length > region.Address + region.Length - address)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
_mappedRegionNames[region.Address] = name;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rVjRvHJ0X6c",
|
||||
ExportName = "sceKernelVirtualQuery",
|
||||
@@ -2696,11 +2846,16 @@ public static class KernelMemoryCompatExports
|
||||
BinaryPrimitives.WriteInt32LittleEndian(payload[28..32], memoryType);
|
||||
payload[32] = unchecked((byte)stateFlags);
|
||||
|
||||
var name = region.IsDirect
|
||||
? "direct"
|
||||
: region.IsFlexible
|
||||
? "flexible"
|
||||
: string.Empty;
|
||||
string name;
|
||||
lock (_memoryGate)
|
||||
{
|
||||
name = _mappedRegionNames.GetValueOrDefault(region.Address)
|
||||
?? (region.IsDirect
|
||||
? "direct"
|
||||
: region.IsFlexible
|
||||
? "flexible"
|
||||
: string.Empty);
|
||||
}
|
||||
if (!string.IsNullOrEmpty(name))
|
||||
{
|
||||
var nameBytes = Encoding.ASCII.GetBytes(name);
|
||||
@@ -2748,7 +2903,7 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (protectionOut != 0 && !TryWriteInt32(ctx, protectionOut, region.Protection))
|
||||
if (protectionOut != 0 && !ctx.TryWriteInt32(protectionOut, region.Protection))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -2787,7 +2942,7 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
if (!ctx.TryWriteUInt64(infoAddress, block.Start) ||
|
||||
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), block.Start + block.Length) ||
|
||||
!TryWriteInt32(ctx, infoAddress + (sizeof(ulong) * 2), block.MemoryType))
|
||||
!ctx.TryWriteInt32(infoAddress + (sizeof(ulong) * 2), block.MemoryType))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -3645,7 +3800,7 @@ public static class KernelMemoryCompatExports
|
||||
var addr = argumentSource.NextGpArg();
|
||||
if (addr != 0)
|
||||
{
|
||||
_ = TryWriteInt32(ctx, addr, sb.Length);
|
||||
_ = ctx.TryWriteInt32(addr, sb.Length);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -3842,118 +3997,48 @@ public static class KernelMemoryCompatExports
|
||||
ulong alignment,
|
||||
out ulong mappedAddress)
|
||||
{
|
||||
mappedAddress = 0;
|
||||
if (length == 0)
|
||||
var executable = (protection & OrbisProtCpuExec) != 0;
|
||||
return KernelVirtualRangeAllocator.TryReserve(
|
||||
ctx,
|
||||
desiredAddress,
|
||||
length,
|
||||
executable,
|
||||
alignment,
|
||||
allowSearch: true,
|
||||
allowAllocateAtAlternative: false,
|
||||
"reserve range",
|
||||
out mappedAddress);
|
||||
}
|
||||
|
||||
private static bool TryReserveExactGuestVirtualRange(
|
||||
CpuContext ctx,
|
||||
ulong desiredAddress,
|
||||
ulong length,
|
||||
int protection)
|
||||
{
|
||||
var executable = (protection & OrbisProtCpuExec) != 0;
|
||||
return KernelVirtualRangeAllocator.TryReserve(
|
||||
ctx,
|
||||
desiredAddress,
|
||||
length,
|
||||
executable,
|
||||
alignment: 0,
|
||||
allowSearch: false,
|
||||
allowAllocateAtAlternative: false,
|
||||
"reserve fixed range",
|
||||
out _);
|
||||
}
|
||||
|
||||
private static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
|
||||
{
|
||||
if (address == 0 || length == 0 || ulong.MaxValue - address < length - 1)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object memoryObject = ctx.Memory;
|
||||
MethodInfo? allocateAt = null;
|
||||
MethodInfo? allocateAtOrAbove = null;
|
||||
var allocateAtHasAllowAlternativeArg = false;
|
||||
for (var depth = 0; depth < 4; depth++)
|
||||
{
|
||||
foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocateAtOrAbove is not null && allocateAt is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocateAtOrAbove is not null || 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;
|
||||
}
|
||||
|
||||
var executable = (protection & OrbisProtCpuExec) != 0;
|
||||
if (allocateAtOrAbove is not null)
|
||||
{
|
||||
var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL };
|
||||
var searchResult = allocateAtOrAbove.Invoke(memoryObject, searchArgs);
|
||||
if (searchResult is bool trueValue && trueValue &&
|
||||
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
||||
{
|
||||
mappedAddress = searchedAddress;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocateAt is null)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve range: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var invokeArgs = allocateAtHasAllowAlternativeArg
|
||||
? new object[] { desiredAddress, length, executable, false }
|
||||
: new object[] { desiredAddress, length, executable };
|
||||
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 range: AllocateAt returned {resultType} value={result ?? "null"}");
|
||||
return false;
|
||||
}
|
||||
|
||||
mappedAddress = allocated;
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][TRACE] reserve range threw while invoking AllocateAt");
|
||||
return false;
|
||||
}
|
||||
Span<byte> probe = stackalloc byte[1];
|
||||
return ctx.Memory.TryRead(address, probe) &&
|
||||
ctx.Memory.TryRead(address + length - 1, probe);
|
||||
}
|
||||
|
||||
private static bool IsMappedGuestRangeAvailable(
|
||||
@@ -4033,6 +4118,11 @@ public static class KernelMemoryCompatExports
|
||||
return guestPath;
|
||||
}
|
||||
|
||||
if (TryResolveRegisteredGuestMount(guestPath, out var mountedPath))
|
||||
{
|
||||
return mountedPath;
|
||||
}
|
||||
|
||||
if (guestPath.StartsWith("/devlog/app/", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
var relative = NormalizeMountRelativePath(guestPath["/devlog/app/".Length..]);
|
||||
@@ -4101,6 +4191,20 @@ public static class KernelMemoryCompatExports
|
||||
var app0Root = ResolveApp0Root();
|
||||
if (!string.IsNullOrWhiteSpace(app0Root))
|
||||
{
|
||||
if (string.Equals(guestPath, "$", StringComparison.Ordinal) ||
|
||||
string.Equals(guestPath, "$/", StringComparison.Ordinal) ||
|
||||
string.Equals(guestPath, "$\\", StringComparison.Ordinal))
|
||||
{
|
||||
return app0Root;
|
||||
}
|
||||
|
||||
if (guestPath.StartsWith("$/", StringComparison.Ordinal) ||
|
||||
guestPath.StartsWith("$\\", StringComparison.Ordinal))
|
||||
{
|
||||
var relative = NormalizeMountRelativePath(guestPath[2..]);
|
||||
return Path.Combine(app0Root, relative);
|
||||
}
|
||||
|
||||
if (string.Equals(guestPath, "/app0", StringComparison.OrdinalIgnoreCase) ||
|
||||
string.Equals(guestPath, "app0", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -4131,6 +4235,51 @@ public static class KernelMemoryCompatExports
|
||||
return guestPath;
|
||||
}
|
||||
|
||||
private static bool TryResolveRegisteredGuestMount(string guestPath, out string hostPath)
|
||||
{
|
||||
hostPath = string.Empty;
|
||||
var normalizedGuestPath = NormalizeGuestStatCachePath(guestPath);
|
||||
if (normalizedGuestPath is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
string? matchedMountPoint = null;
|
||||
string? matchedHostRoot = null;
|
||||
lock (_guestMountGate)
|
||||
{
|
||||
foreach (var (mountPoint, hostRoot) in _guestMounts)
|
||||
{
|
||||
if ((string.Equals(normalizedGuestPath, mountPoint, StringComparison.OrdinalIgnoreCase) ||
|
||||
normalizedGuestPath.StartsWith(mountPoint + "/", StringComparison.OrdinalIgnoreCase)) &&
|
||||
(matchedMountPoint is null || mountPoint.Length > matchedMountPoint.Length))
|
||||
{
|
||||
matchedMountPoint = mountPoint;
|
||||
matchedHostRoot = hostRoot;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (matchedMountPoint is null || matchedHostRoot is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var relativePath = normalizedGuestPath[matchedMountPoint.Length..].TrimStart('/');
|
||||
var candidate = Path.GetFullPath(Path.Combine(
|
||||
matchedHostRoot,
|
||||
NormalizeMountRelativePath(relativePath)));
|
||||
var rootWithSeparator = Path.TrimEndingDirectorySeparator(matchedHostRoot) + Path.DirectorySeparatorChar;
|
||||
if (!string.Equals(candidate, matchedHostRoot, StringComparison.OrdinalIgnoreCase) &&
|
||||
!candidate.StartsWith(rootWithSeparator, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
hostPath = candidate;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static string? ResolveApp0Root()
|
||||
{
|
||||
var cached = Volatile.Read(ref _cachedApp0Root);
|
||||
@@ -4376,7 +4525,7 @@ public static class KernelMemoryCompatExports
|
||||
continue;
|
||||
}
|
||||
|
||||
Span<byte> one = stackalloc byte[1];
|
||||
var one = chunk.AsSpan(0, 1);
|
||||
if (!TryReadCompat(ctx, current, one))
|
||||
{
|
||||
return false;
|
||||
@@ -4815,7 +4964,7 @@ public static class KernelMemoryCompatExports
|
||||
processedCount++;
|
||||
}
|
||||
|
||||
if (processedOutAddress != 0 && !TryWriteInt32(ctx, processedOutAddress, processedCount))
|
||||
if (processedOutAddress != 0 && !ctx.TryWriteInt32(processedOutAddress, processedCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -5652,13 +5801,6 @@ public static class KernelMemoryCompatExports
|
||||
return (protect & expected) != 0;
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BitConverter.TryWriteBytes(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static bool TryWriteOpenDescriptorStat(CpuContext ctx, int fd, ulong statAddress)
|
||||
{
|
||||
if (fd is 0 or 1 or 2)
|
||||
@@ -5718,22 +5860,40 @@ public static class KernelMemoryCompatExports
|
||||
private static bool TryGetAprFileSize(string hostPath, out ulong size)
|
||||
{
|
||||
size = 0;
|
||||
|
||||
string cachePath;
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(hostPath);
|
||||
cachePath = Path.GetFullPath(hostPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
cachePath = hostPath;
|
||||
}
|
||||
|
||||
if (_aprFileSizeCache.TryGetValue(cachePath, out size))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var fileInfo = new FileInfo(cachePath);
|
||||
if (fileInfo.Exists)
|
||||
{
|
||||
var length = fileInfo.Length;
|
||||
size = length < 0 ? 0UL : unchecked((ulong)length);
|
||||
_aprFileSizeCache.TryAdd(cachePath, size);
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!new DirectoryInfo(hostPath).Exists)
|
||||
if (!new DirectoryInfo(cachePath).Exists)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
size = 65536;
|
||||
_aprFileSizeCache.TryAdd(cachePath, size);
|
||||
return true;
|
||||
}
|
||||
catch
|
||||
@@ -6032,6 +6192,20 @@ public static class KernelMemoryCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
private static void InvalidateAprFileSizeCache(string hostPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
hostPath = Path.GetFullPath(hostPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// The cache key remains the original path when normalization fails.
|
||||
}
|
||||
|
||||
_aprFileSizeCache.TryRemove(hostPath, out _);
|
||||
}
|
||||
|
||||
private static string PreviewIoBytes(byte[] buffer, int count, int maxBytes)
|
||||
{
|
||||
if (count <= 0)
|
||||
|
||||
@@ -3,8 +3,6 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -95,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",
|
||||
@@ -322,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",
|
||||
@@ -376,42 +387,42 @@ public static class KernelPthreadCompatExports
|
||||
var initRoutine = ctx[CpuRegister.Rsi];
|
||||
if (onceAddress == 0 || initRoutine == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!TryReadInt32(ctx, onceAddress, out var onceValue))
|
||||
if (!ctx.TryReadInt32(onceAddress, out var onceValue))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (onceValue == PthreadOnceDone)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
var gate = GetPthreadOnceGate(onceAddress);
|
||||
var shouldCall = false;
|
||||
lock (gate)
|
||||
{
|
||||
if (!TryReadInt32(ctx, onceAddress, out onceValue))
|
||||
if (!ctx.TryReadInt32(onceAddress, out onceValue))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
while (onceValue == PthreadOnceInProgress)
|
||||
{
|
||||
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
|
||||
if (!TryReadInt32(ctx, onceAddress, out onceValue))
|
||||
if (!ctx.TryReadInt32(onceAddress, out onceValue))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (onceValue != PthreadOnceDone)
|
||||
{
|
||||
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceInProgress))
|
||||
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceInProgress))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
shouldCall = true;
|
||||
@@ -427,21 +438,21 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
}
|
||||
|
||||
TracePthreadOnce(onceAddress, initRoutine, "failed", error);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
lock (gate)
|
||||
{
|
||||
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceDone))
|
||||
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceDone))
|
||||
{
|
||||
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Monitor.PulseAll(gate);
|
||||
@@ -449,7 +460,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||
@@ -1011,19 +1022,12 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
||||
TryWriteUInt32(ctx, address + 0x20, unchecked((uint)state.Type)) &&
|
||||
TryWriteUInt32(ctx, address + 0x3C, unchecked((uint)state.Protocol));
|
||||
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) =>
|
||||
TryWriteUInt32(ctx, address, unchecked((uint)state.Type)) &&
|
||||
TryWriteUInt32(ctx, address + 4, unchecked((uint)state.Protocol));
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BitConverter.TryWriteBytes(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
ctx.TryWriteUInt32(address, unchecked((uint)state.Type)) &&
|
||||
ctx.TryWriteUInt32(address + 4, unchecked((uint)state.Protocol));
|
||||
|
||||
private static int PthreadCondInitCore(CpuContext ctx, ulong condAddress)
|
||||
{
|
||||
@@ -1315,32 +1319,6 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
|
||||
{
|
||||
var createdState = new PthreadMutexState
|
||||
|
||||
@@ -2,10 +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;
|
||||
@@ -22,6 +20,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
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();
|
||||
@@ -31,6 +30,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
private static int _nextTlsKey = 1;
|
||||
private static long _nextSyntheticRwlockHandleId = 1;
|
||||
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
||||
private static long _nextSyntheticRwlockAttrHandleId = 1;
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
||||
|
||||
@@ -277,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;
|
||||
}
|
||||
@@ -324,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;
|
||||
}
|
||||
@@ -494,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;
|
||||
}
|
||||
@@ -717,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",
|
||||
@@ -731,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;
|
||||
}
|
||||
@@ -810,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",
|
||||
@@ -987,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",
|
||||
@@ -1011,12 +1112,12 @@ public static class KernelPthreadExtendedCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1348,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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,15 +4,11 @@
|
||||
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;
|
||||
|
||||
@@ -87,11 +83,11 @@ public static class KernelRuntimeCompatExports
|
||||
|
||||
if (micros < 1000)
|
||||
{
|
||||
// Guest worker pools use usleep(1) as a polling backoff. Periodically
|
||||
// relinquish a full host time slice so spin workers cannot starve producers.
|
||||
if ((++_shortUsleepCount & 31) == 0)
|
||||
// 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.Sleep(1);
|
||||
Thread.Sleep(0);
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -260,8 +256,8 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
|
||||
if (timezoneAddress != 0 &&
|
||||
(!TryWriteInt32(ctx, timezoneAddress, 0) ||
|
||||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), 0)))
|
||||
(!ctx.TryWriteInt32(timezoneAddress, 0) ||
|
||||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), 0)))
|
||||
{
|
||||
return -1;
|
||||
}
|
||||
@@ -618,7 +614,7 @@ public static class KernelRuntimeCompatExports
|
||||
internal static bool TrySetErrno(CpuContext ctx, int value)
|
||||
{
|
||||
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
||||
return address != 0 && TryWriteInt32(ctx, address, value);
|
||||
return address != 0 && ctx.TryWriteInt32(address, value);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -739,8 +735,11 @@ public static class KernelRuntimeCompatExports
|
||||
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} flags=0x{flags:X8} align=0x{effectiveAlignment: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))
|
||||
{
|
||||
@@ -752,6 +751,7 @@ public static class KernelRuntimeCompatExports
|
||||
_nextReservedVirtualBase = Math.Max(_nextReservedVirtualBase, mappedAddress + length);
|
||||
}
|
||||
|
||||
KernelMemoryCompatExports.RegisterReservedVirtualRange(mappedAddress, length);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -824,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;
|
||||
}
|
||||
@@ -854,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;
|
||||
}
|
||||
@@ -1055,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;
|
||||
}
|
||||
@@ -1073,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;
|
||||
}
|
||||
@@ -1230,8 +1230,8 @@ public static class KernelRuntimeCompatExports
|
||||
: 0;
|
||||
var minutesWest = unchecked((int)-offset.TotalMinutes);
|
||||
|
||||
if (!TryWriteInt32(ctx, timezoneAddress, minutesWest) ||
|
||||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), dstSeconds / 60))
|
||||
if (!ctx.TryWriteInt32(timezoneAddress, minutesWest) ||
|
||||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), dstSeconds / 60))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1241,7 +1241,7 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (dstSecondsAddress != 0 && !TryWriteInt32(ctx, dstSecondsAddress, dstSeconds))
|
||||
if (dstSecondsAddress != 0 && !ctx.TryWriteInt32(dstSecondsAddress, dstSeconds))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1301,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;
|
||||
}
|
||||
@@ -1391,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;
|
||||
}
|
||||
@@ -1428,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;
|
||||
}
|
||||
@@ -1469,26 +1469,42 @@ public static class KernelRuntimeCompatExports
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new List<byte>(Math.Min(maxLength, 64));
|
||||
Span<byte> one = stackalloc byte[1];
|
||||
for (var i = 0; i < maxLength; i++)
|
||||
const int pageSize = 4096;
|
||||
const int inlineChunkSize = 64;
|
||||
Span<byte> buffer = stackalloc byte[Math.Min(maxLength, 512)];
|
||||
var length = 0;
|
||||
|
||||
for (var offset = 0; offset < maxLength && length < buffer.Length;)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(address + (ulong)i, one))
|
||||
var current = address + (ulong)offset;
|
||||
var pageRemaining = pageSize - (int)(current & (pageSize - 1));
|
||||
var chunkSize = Math.Min(
|
||||
buffer.Length - length,
|
||||
Math.Min(maxLength - offset, Math.Min(inlineChunkSize, pageRemaining)));
|
||||
|
||||
if (chunkSize <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (one[0] == 0)
|
||||
var chunk = buffer.Slice(length, chunkSize);
|
||||
if (!ctx.Memory.TryRead(current, chunk))
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes.ToArray());
|
||||
return false;
|
||||
}
|
||||
|
||||
var nulIndex = chunk.IndexOf((byte)0);
|
||||
if (nulIndex >= 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(buffer[..(length + nulIndex)]);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes.Add(one[0]);
|
||||
length += chunkSize;
|
||||
offset += chunkSize;
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes.ToArray());
|
||||
return true;
|
||||
return false;
|
||||
}
|
||||
|
||||
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
||||
@@ -1501,13 +1517,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
|
||||
@@ -1732,117 +1741,16 @@ public static class KernelRuntimeCompatExports
|
||||
bool allowSearch,
|
||||
out ulong mappedAddress)
|
||||
{
|
||||
mappedAddress = 0;
|
||||
if (length == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
object memoryObject = ctx.Memory;
|
||||
MethodInfo? allocateAt = null;
|
||||
MethodInfo? allocateAtOrAbove = null;
|
||||
var allocateAtHasAllowAlternativeArg = false;
|
||||
for (var depth = 0; depth < 4; depth++)
|
||||
{
|
||||
foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocateAtOrAbove is not null && allocateAt is not null)
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocateAtOrAbove is not null || 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 (allowSearch && allocateAtOrAbove is not null)
|
||||
{
|
||||
var searchArgs = new object[] { desiredAddress, length, false, alignment, 0UL };
|
||||
var searchResult = allocateAtOrAbove.Invoke(memoryObject, searchArgs);
|
||||
if (searchResult is bool trueValue && trueValue &&
|
||||
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
||||
{
|
||||
mappedAddress = searchedAddress;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if (allocateAt is null)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
||||
return false;
|
||||
}
|
||||
|
||||
var invokeArgs = allocateAtHasAllowAlternativeArg
|
||||
? new object[] { desiredAddress, length, false, allowSearch }
|
||||
: new object[] { desiredAddress, length, false };
|
||||
var result = allocateAt.Invoke(memoryObject, invokeArgs);
|
||||
if (result is not ulong allocated || allocated == 0)
|
||||
{
|
||||
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)
|
||||
@@ -1855,4 +1763,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(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);
|
||||
}
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
@@ -32,7 +30,7 @@ public static class Http2Exports
|
||||
|
||||
if (poolSize == 0 || maxRequests <= 0)
|
||||
{
|
||||
return SetReturn(ctx, Http2ErrorInvalidArgument);
|
||||
return ctx.SetReturn(Http2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextContextId);
|
||||
@@ -53,17 +51,11 @@ public static class Http2Exports
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!_contexts.TryRemove(id, out _))
|
||||
{
|
||||
return SetReturn(ctx, Http2ErrorInvalidId);
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
TraceHttp2("term", id, 0, 0, 0, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static void TraceHttp2(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
|
||||
@@ -33,7 +32,7 @@ public static class HttpExports
|
||||
var poolSize = ctx[CpuRegister.Rdx];
|
||||
if (poolSize == 0)
|
||||
{
|
||||
return SetReturn(ctx, HttpErrorInvalidValue);
|
||||
return ctx.SetReturn(HttpErrorInvalidValue);
|
||||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextContextId);
|
||||
@@ -53,7 +52,7 @@ public static class HttpExports
|
||||
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!Contexts.ContainsKey(contextId))
|
||||
{
|
||||
return SetReturn(ctx, HttpErrorInvalidId);
|
||||
return ctx.SetReturn(HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
var userAgentAddress = ctx[CpuRegister.Rsi];
|
||||
@@ -75,8 +74,8 @@ public static class HttpExports
|
||||
{
|
||||
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return Templates.TryRemove(templateId, out _)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, HttpErrorInvalidId);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -89,7 +88,7 @@ public static class HttpExports
|
||||
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!Contexts.TryRemove(contextId, out _))
|
||||
{
|
||||
return SetReturn(ctx, HttpErrorInvalidId);
|
||||
return ctx.SetReturn(HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
foreach (var pair in Templates)
|
||||
@@ -100,13 +99,7 @@ public static class HttpExports
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
||||
|
||||
@@ -57,27 +57,27 @@ public static class NetCtlExports
|
||||
var natInfoAddress = ctx[CpuRegister.Rdi];
|
||||
if (natInfoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> natInfo = stackalloc byte[NatInfoSize];
|
||||
if (!ctx.Memory.TryRead(natInfoAddress, natInfo))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]);
|
||||
if (size != NatInfoSize)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
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)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -87,7 +87,7 @@ public static class NetCtlExports
|
||||
LibraryName = "libSceNetCtl")]
|
||||
public static int NetCtlCheckCallback(CpuContext ctx)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -100,14 +100,14 @@ public static class NetCtlExports
|
||||
var stateAddress = ctx[CpuRegister.Rdi];
|
||||
if (stateAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> stateBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0);
|
||||
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -122,7 +122,7 @@ public static class NetCtlExports
|
||||
var callbackIdAddress = ctx[CpuRegister.Rdx];
|
||||
if (function == 0 || callbackIdAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
lock (CallbackGate)
|
||||
@@ -130,20 +130,20 @@ public static class NetCtlExports
|
||||
var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0);
|
||||
if (callbackId < 0)
|
||||
{
|
||||
return SetReturn(ctx, NetCtlErrorNoSpace);
|
||||
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 SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
Callbacks[callbackId] = new CallbackRegistration(function, argument);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -157,7 +157,7 @@ public static class NetCtlExports
|
||||
var infoAddress = ctx[CpuRegister.Rsi];
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
}
|
||||
|
||||
return code switch
|
||||
@@ -177,7 +177,7 @@ public static class NetCtlExports
|
||||
NetCtlInfoHttpProxyConfig => WriteUInt32(ctx, infoAddress, 0),
|
||||
NetCtlInfoHttpProxyServer => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
|
||||
NetCtlInfoHttpProxyPort => WriteUInt16(ctx, infoAddress, 0),
|
||||
_ => SetReturn(ctx, NetCtlErrorNotConnected),
|
||||
_ => ctx.SetReturn(NetCtlErrorNotConnected, typeof(long)),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -185,8 +185,8 @@ public static class NetCtlExports
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[count];
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? 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)
|
||||
@@ -194,8 +194,8 @@ public static class NetCtlExports
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? 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)
|
||||
@@ -203,8 +203,8 @@ public static class NetCtlExports
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? 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)
|
||||
@@ -217,13 +217,7 @@ public static class NetCtlExports
|
||||
}
|
||||
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
|
||||
return result;
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,9 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
@@ -35,7 +33,7 @@ public static class NetExports
|
||||
{
|
||||
_initialized = true;
|
||||
TraceNet("init", 0, 0, 0, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -49,7 +47,7 @@ public static class NetExports
|
||||
_pools.Clear();
|
||||
_resolvers.Clear();
|
||||
TraceNet("term", 0, 0, 0, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -65,7 +63,7 @@ public static class NetExports
|
||||
|
||||
if (size <= 0)
|
||||
{
|
||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||
@@ -90,11 +88,11 @@ public static class NetExports
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!_pools.TryRemove(id, out _))
|
||||
{
|
||||
return SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||
return ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||
}
|
||||
|
||||
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -109,7 +107,7 @@ public static class NetExports
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
if (flags != 0)
|
||||
{
|
||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||
@@ -131,8 +129,8 @@ public static class NetExports
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return _resolvers.TryRemove(id, out _)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -146,25 +144,19 @@ public static class NetExports
|
||||
var statusAddress = ctx[CpuRegister.Rsi];
|
||||
if (statusAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||
}
|
||||
|
||||
if (!_resolvers.TryGetValue(id, out var resolver))
|
||||
{
|
||||
return SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||
return ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||
}
|
||||
|
||||
Span<byte> status = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
|
||||
return ctx.Memory.TryWrite(statusAddress, status)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
? 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)
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
@@ -28,7 +26,7 @@ public static class SslExports
|
||||
var poolSize = ctx[CpuRegister.Rdi];
|
||||
if (poolSize == 0)
|
||||
{
|
||||
return SetReturn(ctx, SslErrorOutOfSize);
|
||||
return ctx.SetReturn(SslErrorOutOfSize);
|
||||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextContextId);
|
||||
@@ -49,11 +47,11 @@ public static class SslExports
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!_contexts.TryRemove(id, out _))
|
||||
{
|
||||
return SetReturn(ctx, SslErrorInvalidId);
|
||||
return ctx.SetReturn(SslErrorInvalidId);
|
||||
}
|
||||
|
||||
TraceSsl("term", id, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -65,13 +63,7 @@ public static class SslExports
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
TraceSsl("close", id, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static void TraceSsl(string operation, int id, ulong arg0)
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -26,12 +26,12 @@ public static class NpEntitlementAccessExports
|
||||
clear.Clear();
|
||||
if (!ctx.Memory.TryWrite(bootParam, clear))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -48,20 +48,14 @@ public static class NpEntitlementAccessExports
|
||||
emptyList.Clear();
|
||||
if (!ctx.Memory.TryWrite(listAddress, emptyList))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static void TraceNpEntitlementAccess(string message)
|
||||
|
||||
@@ -76,14 +76,14 @@ public static class NpManagerExports
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (stateAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
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)
|
||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -97,7 +97,7 @@ public static class NpManagerExports
|
||||
var titleSecretAddress = ctx[CpuRegister.Rsi];
|
||||
if (titleIdAddress == 0 || titleSecretAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> titleId = stackalloc byte[NpTitleIdSize];
|
||||
@@ -105,17 +105,11 @@ public static class NpManagerExports
|
||||
if (!ctx.Memory.TryRead(titleIdAddress, titleId) ||
|
||||
!ctx.Memory.TryRead(titleSecretAddress, titleSecret))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceNp($"set_np_title_id title='{ReadTitleId(titleId)}'");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Np;
|
||||
|
||||
@@ -22,13 +21,13 @@ public static class NpUniversalDataSystemExports
|
||||
var parameterAddress = ctx[CpuRegister.Rdi];
|
||||
if (parameterAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, NpUniversalDataSystemErrorInvalidArgument);
|
||||
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> parameters = stackalloc byte[16];
|
||||
return ctx.Memory.TryRead(parameterAddress, parameters)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -41,14 +40,14 @@ public static class NpUniversalDataSystemExports
|
||||
var contextAddress = ctx[CpuRegister.Rdi];
|
||||
if (contextAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
|
||||
Span<byte> context = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(context, 1);
|
||||
return ctx.Memory.TryWrite(contextAddress, context)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -59,13 +58,13 @@ public static class NpUniversalDataSystemExports
|
||||
public static int NpUniversalDataSystemCreateHandle(CpuContext ctx)
|
||||
{
|
||||
var handle = Interlocked.Increment(ref _nextHandle);
|
||||
if (TryWriteInt32(ctx, ctx[CpuRegister.Rdi], handle) ||
|
||||
TryWriteInt32(ctx, ctx[CpuRegister.Rsi], handle))
|
||||
if (ctx.TryWriteInt32(ctx[CpuRegister.Rdi], handle, checkNil: true) ||
|
||||
ctx.TryWriteInt32(ctx[CpuRegister.Rsi], handle, checkNil: true))
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -75,24 +74,6 @@ public static class NpUniversalDataSystemExports
|
||||
LibraryName = "libSceNpUniversalDataSystem")]
|
||||
public static int NpUniversalDataSystemRegisterContext(CpuContext ctx)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
if (address == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Np;
|
||||
@@ -25,12 +23,12 @@ public static class NpWebApi2Exports
|
||||
|
||||
if (httpContextId <= 0 || poolSize == 0)
|
||||
{
|
||||
return SetReturn(ctx, NpWebApi2ErrorInvalidArgument);
|
||||
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
TraceNpWebApi2("init", httpContextId, poolSize);
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -43,13 +41,7 @@ public static class NpWebApi2Exports
|
||||
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
Interlocked.Exchange(ref _initialized, 0);
|
||||
TraceNpWebApi2("term", libraryContextId, 0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static void TraceNpWebApi2(string operation, int id, ulong arg0)
|
||||
|
||||
@@ -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,17 +126,17 @@ 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)
|
||||
? 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(
|
||||
@@ -145,27 +151,139 @@ public static class PadExports
|
||||
var count = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
if (handle != PrimaryPadHandle)
|
||||
{
|
||||
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
|
||||
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
|
||||
}
|
||||
|
||||
if (dataAddress == 0 || count < 1 || count > 64)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
return WriteNeutralPadData(ctx, dataAddress)
|
||||
? SetReturn(ctx, 1)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? 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();
|
||||
@@ -180,9 +298,63 @@ public static class PadExports
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -234,7 +234,7 @@ public static class PlayGoExports
|
||||
if (outChunkIdList == 0)
|
||||
{
|
||||
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
|
||||
return TryWriteUInt32(ctx, outEntries, availableEntries)
|
||||
return ctx.TryWriteUInt32(outEntries, availableEntries)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -249,14 +249,14 @@ public static class PlayGoExports
|
||||
for (uint i = 0; i < entriesToWrite; i++)
|
||||
{
|
||||
var chunkId = chunkIds.Length == 0 ? (ushort)0 : chunkIds[i];
|
||||
if (!TryWriteUInt16(ctx, outChunkIdList + (i * sizeof(ushort)), chunkId))
|
||||
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 TryWriteUInt32(ctx, outEntries, entriesToWrite)
|
||||
return ctx.TryWriteUInt32(outEntries, entriesToWrite)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -292,7 +292,7 @@ public static class PlayGoExports
|
||||
|
||||
return ValidateChunkIds(ctx, chunkIds, numberOfEntries) is { } chunkError && chunkError != 0
|
||||
? chunkError
|
||||
: TryWriteInt64(ctx, outEta, 0)
|
||||
: ctx.TryWriteInt64(outEta, 0)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -324,7 +324,7 @@ public static class PlayGoExports
|
||||
speed = _installSpeed;
|
||||
}
|
||||
|
||||
return TryWriteInt32(ctx, outSpeed, speed)
|
||||
return ctx.TryWriteInt32(outSpeed, speed)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -399,7 +399,7 @@ public static class PlayGoExports
|
||||
var loci = new byte[numberOfEntries];
|
||||
for (uint i = 0; i < numberOfEntries; i++)
|
||||
{
|
||||
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId))
|
||||
if (!ctx.TryReadUInt16(chunkIds + (i * sizeof(ushort)), out var chunkId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -503,7 +503,7 @@ public static class PlayGoExports
|
||||
}
|
||||
|
||||
TracePlayGo($"get_todo requested={numberOfEntries} wrote=0");
|
||||
return TryWriteUInt32(ctx, outEntries, 0)
|
||||
return ctx.TryWriteUInt32(outEntries, 0)
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -648,7 +648,7 @@ public static class PlayGoExports
|
||||
{
|
||||
for (uint i = 0; i < numberOfEntries; i++)
|
||||
{
|
||||
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId))
|
||||
if (!ctx.TryReadUInt16(chunkIds + (i * sizeof(ushort)), out var chunkId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -728,47 +728,6 @@ public static class PlayGoExports
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt16LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt16(CpuContext ctx, ulong address, ushort value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryWriteInt64(CpuContext ctx, ulong address, long value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(long)];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static void TracePlayGo(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PLAYGO"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.SaveData;
|
||||
|
||||
@@ -35,10 +34,10 @@ public static class SaveDataDialogExports
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
|
||||
{
|
||||
return SetReturn(ctx, ErrorAlreadyInitialized);
|
||||
return ctx.SetReturn(ErrorAlreadyInitialized);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -51,22 +50,22 @@ public static class SaveDataDialogExports
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, ErrorArgNull);
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (_status is not (StatusInitialized or StatusFinished))
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotInitialized);
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
|
||||
_lastUserData = TryReadUInt64(ctx, paramAddress + 0xC8, out var userData) ? userData : 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 SetReturn(ctx, ErrorOk);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -74,21 +73,21 @@ public static class SaveDataDialogExports
|
||||
ExportName = "sceSaveDataDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
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) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
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) => SetReturn(ctx, 1);
|
||||
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => ctx.SetReturn(1);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yEiJ-qqr6Cg",
|
||||
@@ -100,12 +99,12 @@ public static class SaveDataDialogExports
|
||||
var resultAddress = ctx[CpuRegister.Rdi];
|
||||
if (resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, ErrorArgNull);
|
||||
return ctx.SetReturn(ErrorArgNull);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _status) != StatusFinished)
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotFinished);
|
||||
return ctx.SetReturn(ErrorNotFinished);
|
||||
}
|
||||
|
||||
Span<byte> result = stackalloc byte[ResultSize];
|
||||
@@ -117,10 +116,10 @@ public static class SaveDataDialogExports
|
||||
|
||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -132,10 +131,10 @@ public static class SaveDataDialogExports
|
||||
{
|
||||
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotRunning);
|
||||
return ctx.SetReturn(ErrorNotRunning);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -147,12 +146,12 @@ public static class SaveDataDialogExports
|
||||
{
|
||||
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
|
||||
{
|
||||
return SetReturn(ctx, ErrorNotInitialized);
|
||||
return ctx.SetReturn(ErrorNotInitialized);
|
||||
}
|
||||
|
||||
_lastMode = 0;
|
||||
_lastUserData = 0;
|
||||
return SetReturn(ctx, ErrorOk);
|
||||
return ctx.SetReturn(ErrorOk);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -160,14 +159,14 @@ public static class SaveDataDialogExports
|
||||
ExportName = "sceSaveDataDialogProgressBarInc",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceSaveDataDialog")]
|
||||
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => SetReturn(ctx, ErrorOk);
|
||||
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) => SetReturn(ctx, ErrorOk);
|
||||
public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => ctx.SetReturn(ErrorOk);
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
@@ -182,25 +181,6 @@ public static class SaveDataDialogExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
value = 0;
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceSaveDataDialog(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
@@ -10,6 +11,8 @@ 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;
|
||||
@@ -23,6 +26,9 @@ public static class SaveDataExports
|
||||
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;
|
||||
|
||||
@@ -44,15 +50,15 @@ public static class SaveDataExports
|
||||
try
|
||||
{
|
||||
Directory.CreateDirectory(ResolveSaveDataRoot());
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -67,18 +73,18 @@ public static class SaveDataExports
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
if (condAddress == 0 || resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
|
||||
!TryReadSearchResult(ctx, resultAddress, out var result))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -90,7 +96,7 @@ public static class SaveDataExports
|
||||
}
|
||||
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
|
||||
@@ -102,21 +108,21 @@ public static class SaveDataExports
|
||||
var setNum = result.DirNamesNum == 0
|
||||
? 0
|
||||
: Math.Min(result.DirNamesNum, entries.Count);
|
||||
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
||||
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
||||
if (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
||||
!ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
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 SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
if (result.DirNamesAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
for (var i = 0; i < setNum; i++)
|
||||
@@ -132,31 +138,150 @@ public static class SaveDataExports
|
||||
(result.InfosAddress != 0 &&
|
||||
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
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 SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
return ctx.SetReturn(OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
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 (!TryReadInt32(ctx, address, out var userId) ||
|
||||
if (!ctx.TryReadInt32(address, out var userId) ||
|
||||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
||||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
||||
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
|
||||
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
|
||||
!ctx.TryReadUInt32(address + 0x18, out var sortKey) ||
|
||||
!ctx.TryReadUInt32(address + 0x1C, out var sortOrder))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -179,7 +304,7 @@ public static class SaveDataExports
|
||||
{
|
||||
result = default;
|
||||
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
|
||||
!TryReadUInt32(ctx, address + ResultDirNamesNumOffset, out var dirNamesNum) ||
|
||||
!ctx.TryReadUInt32(address + ResultDirNamesNumOffset, out var dirNamesNum) ||
|
||||
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
|
||||
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
|
||||
{
|
||||
@@ -305,7 +430,7 @@ public static class SaveDataExports
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
|
||||
var root = string.IsNullOrWhiteSpace(configured)
|
||||
? Path.Combine(Environment.CurrentDirectory, "user", "savedata")
|
||||
? Path.Combine(AppContext.BaseDirectory, "user", "savedata")
|
||||
: configured;
|
||||
return Path.GetFullPath(root);
|
||||
}
|
||||
@@ -380,45 +505,6 @@ public static class SaveDataExports
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceSaveData(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SAVEDATA"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Share;
|
||||
@@ -26,13 +25,13 @@ public static class ShareExports
|
||||
var affinityMask = ctx[CpuRegister.Rdx];
|
||||
if (memorySize == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -45,12 +44,12 @@ public static class ShareExports
|
||||
var contentParamAddress = ctx[CpuRegister.Rdi];
|
||||
if (contentParamAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!TryReadNullTerminatedUtf8(ctx, contentParamAddress, MaxContentParamBytes, out var contentParam))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
_contentParam = contentParam;
|
||||
@@ -60,13 +59,7 @@ public static class ShareExports
|
||||
}
|
||||
|
||||
TraceShare($"set_content_param len={contentParam.Length} preview='{FormatTraceString(contentParam)}'");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
|
||||
@@ -10,10 +10,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<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>
|
||||
|
||||
@@ -24,7 +24,7 @@ public static class SystemServiceExports
|
||||
var valueAddress = ctx[CpuRegister.Rsi];
|
||||
if (valueAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
var value = parameterId switch
|
||||
@@ -37,8 +37,8 @@ public static class SystemServiceExports
|
||||
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||
return ctx.Memory.TryWrite(valueAddress, valueBytes)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -51,7 +51,7 @@ public static class SystemServiceExports
|
||||
var statusAddress = ctx[CpuRegister.Rdi];
|
||||
if (statusAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> status = stackalloc byte[SystemServiceStatusSize];
|
||||
@@ -60,8 +60,8 @@ public static class SystemServiceExports
|
||||
status[0x06] = 1;
|
||||
|
||||
return ctx.Memory.TryWrite(statusAddress, status)
|
||||
? 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(
|
||||
@@ -74,7 +74,7 @@ public static class SystemServiceExports
|
||||
var infoAddress = ctx[CpuRegister.Rdi];
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
|
||||
return ctx.SetReturn(OrbisSystemServiceErrorParameter);
|
||||
}
|
||||
|
||||
Span<byte> info = stackalloc byte[DisplaySafeAreaInfoSize];
|
||||
@@ -82,8 +82,8 @@ public static class SystemServiceExports
|
||||
BinaryPrimitives.WriteSingleLittleEndian(info, 1.0f);
|
||||
|
||||
return ctx.Memory.TryWrite(infoAddress, info)
|
||||
? 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(
|
||||
@@ -94,12 +94,6 @@ public static class SystemServiceExports
|
||||
public static int SystemServiceHideSplashScreen(CpuContext ctx)
|
||||
{
|
||||
VulkanVideoPresenter.HideSplashScreen();
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.UserService;
|
||||
|
||||
@@ -40,12 +39,12 @@ public static class UserServiceExports
|
||||
var userIdAddress = ctx[CpuRegister.Rdi];
|
||||
if (userIdAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -58,7 +57,7 @@ public static class UserServiceExports
|
||||
var userIdListAddress = ctx[CpuRegister.Rdi];
|
||||
if (userIdListAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
|
||||
@@ -67,8 +66,8 @@ public static class UserServiceExports
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
|
||||
return ctx.Memory.TryWrite(userIdListAddress, userIds)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -81,20 +80,20 @@ public static class UserServiceExports
|
||||
var eventAddress = ctx[CpuRegister.Rdi];
|
||||
if (eventAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorNoEvent);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorNoEvent);
|
||||
}
|
||||
|
||||
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
|
||||
return ctx.Memory.TryWrite(eventAddress, payload)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -109,25 +108,25 @@ public static class UserServiceExports
|
||||
var capacity = ctx[CpuRegister.Rdx];
|
||||
if (userId != PrimaryUserId)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
||||
}
|
||||
|
||||
if (nameAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
|
||||
if (capacity <= (ulong)nameBytes.Length)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorBufferTooShort);
|
||||
}
|
||||
|
||||
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
||||
nameBytes.CopyTo(output);
|
||||
return ctx.Memory.TryWrite(nameAddress, output)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -141,29 +140,16 @@ public static class UserServiceExports
|
||||
var valueAddress = ctx[CpuRegister.Rsi];
|
||||
if (parameterId != 1000)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
|
||||
}
|
||||
|
||||
if (valueAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
|
||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||
}
|
||||
|
||||
return TryWriteInt32(ctx, valueAddress, 0)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
return ctx.TryWriteInt32(valueAddress, 0)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
using System.Threading;
|
||||
|
||||
@@ -48,6 +49,13 @@ public static class VideoOutExports
|
||||
private static int _frameDumpCount;
|
||||
private static long _nextFrameDumpIndex;
|
||||
private static string _windowTitle = "SharpEmu VideoOut";
|
||||
private static readonly bool _logFrameRate = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
private static long _frameRateWindowStart = Stopwatch.GetTimestamp();
|
||||
private static long _submittedFrameCount;
|
||||
private static long _presentedFrameCount;
|
||||
|
||||
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version)
|
||||
{
|
||||
@@ -812,9 +820,46 @@ public static class VideoOutExports
|
||||
}
|
||||
|
||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
||||
ReportFrameRate(presented: false);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
internal static void ReportPresentedFrame() =>
|
||||
ReportFrameRate(presented: true);
|
||||
|
||||
private static void ReportFrameRate(bool presented)
|
||||
{
|
||||
if (!_logFrameRate)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (presented)
|
||||
{
|
||||
Interlocked.Increment(ref _presentedFrameCount);
|
||||
}
|
||||
else
|
||||
{
|
||||
Interlocked.Increment(ref _submittedFrameCount);
|
||||
}
|
||||
|
||||
var started = Volatile.Read(ref _frameRateWindowStart);
|
||||
var now = Stopwatch.GetTimestamp();
|
||||
var elapsedTicks = now - started;
|
||||
if (elapsedTicks < Stopwatch.Frequency ||
|
||||
Interlocked.CompareExchange(ref _frameRateWindowStart, now, started) != started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var elapsedSeconds = (double)elapsedTicks / Stopwatch.Frequency;
|
||||
var submitted = Interlocked.Exchange(ref _submittedFrameCount, 0);
|
||||
var presentedCount = Interlocked.Exchange(ref _presentedFrameCount, 0);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][PERF] videoout submitted_fps={submitted / elapsedSeconds:F1} " +
|
||||
$"presented_fps={presentedCount / elapsedSeconds:F1}");
|
||||
}
|
||||
|
||||
private static int RegisterBufferRange(VideoOutPortState port, int startIndex, ReadOnlySpan<ulong> addresses, BufferAttribute attribute, int requestedGroupIndex = -1)
|
||||
{
|
||||
lock (_stateGate)
|
||||
@@ -877,9 +922,9 @@ public static class VideoOutExports
|
||||
private static bool TryReadBufferAttribute(CpuContext ctx, ulong attributeAddress, bool attribute2, out BufferAttribute attribute)
|
||||
{
|
||||
attribute = default;
|
||||
if (!TryReadUInt32(ctx, attributeAddress + 0x04, out var tilingMode) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x0C, out var width) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x10, out var height))
|
||||
if (!ctx.TryReadUInt32(attributeAddress + 0x04, out var tilingMode) ||
|
||||
!ctx.TryReadUInt32(attributeAddress + 0x0C, out var width) ||
|
||||
!ctx.TryReadUInt32(attributeAddress + 0x10, out var height))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -896,10 +941,10 @@ public static class VideoOutExports
|
||||
return true;
|
||||
}
|
||||
|
||||
if (!TryReadUInt32(ctx, attributeAddress + 0x00, out var pixelFormat32) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x08, out var aspectRatio) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x14, out var pitchInPixel) ||
|
||||
!TryReadUInt32(ctx, attributeAddress + 0x18, out var option32))
|
||||
if (!ctx.TryReadUInt32(attributeAddress + 0x00, out var pixelFormat32) ||
|
||||
!ctx.TryReadUInt32(attributeAddress + 0x08, out var aspectRatio) ||
|
||||
!ctx.TryReadUInt32(attributeAddress + 0x14, out var pitchInPixel) ||
|
||||
!ctx.TryReadUInt32(attributeAddress + 0x18, out var option32))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -1246,19 +1291,6 @@ public static class VideoOutExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadInt16(CpuContext ctx, ulong address, out short value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(short)];
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -11,6 +11,16 @@
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "Direct",
|
||||
"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": "Direct",
|
||||
"requested": "[2.23.0, )",
|
||||
|
||||
@@ -0,0 +1,68 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Dispatches each <see cref="LogEntry"/> to every child sink.
|
||||
/// Exceptions thrown by a child are swallowed so one failing sink
|
||||
/// (e.g. a closed file) cannot silence the others.
|
||||
/// </summary>
|
||||
public sealed class CompositeLogSink : ISharpEmuLogSink, IDisposable
|
||||
{
|
||||
private readonly ISharpEmuLogSink[] _sinks;
|
||||
private bool _disposed;
|
||||
|
||||
public CompositeLogSink(params ISharpEmuLogSink[] sinks)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sinks);
|
||||
|
||||
foreach (var sink in sinks)
|
||||
{
|
||||
ArgumentNullException.ThrowIfNull(sink, nameof(sinks));
|
||||
}
|
||||
|
||||
_sinks = sinks;
|
||||
}
|
||||
|
||||
public IReadOnlyList<ISharpEmuLogSink> Sinks => _sinks;
|
||||
|
||||
public void Write(in LogEntry entry)
|
||||
{
|
||||
foreach (var sink in _sinks)
|
||||
{
|
||||
try
|
||||
{
|
||||
sink.Write(in entry);
|
||||
}
|
||||
catch
|
||||
{
|
||||
// A broken sink must not prevent the remaining sinks from logging.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
|
||||
foreach (var sink in _sinks)
|
||||
{
|
||||
if (sink is IDisposable disposable)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.IO;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.Logging;
|
||||
|
||||
/// <summary>
|
||||
/// Writes log entries to a file. Thread-safe via an internal lock.
|
||||
/// <see cref="StreamWriter.AutoFlush"/> is enabled so entries survive a crash.
|
||||
/// </summary>
|
||||
public sealed class FileLogSink : ISharpEmuLogSink, IDisposable
|
||||
{
|
||||
private readonly object _sync = new();
|
||||
private readonly StreamWriter _writer;
|
||||
private bool _disposed;
|
||||
|
||||
/// <param name="path">Absolute or relative file path. Parent directories are created if missing.</param>
|
||||
/// <param name="append"><see langword="true"/> to append to an existing file; <see langword="false"/> to overwrite.</param>
|
||||
/// <param name="includeTimestamp">Always recommended for file logs — entries include a full date-time prefix.</param>
|
||||
public FileLogSink(string path, bool append = true, bool includeTimestamp = true)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(path);
|
||||
|
||||
var directory = Path.GetDirectoryName(path);
|
||||
if (!string.IsNullOrEmpty(directory) && !Directory.Exists(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
var fileStream = new FileStream(
|
||||
path,
|
||||
append ? FileMode.Append : FileMode.Create,
|
||||
FileAccess.Write,
|
||||
FileShare.Read,
|
||||
bufferSize: 4096,
|
||||
FileOptions.SequentialScan);
|
||||
_writer = new StreamWriter(fileStream, Encoding.UTF8)
|
||||
{
|
||||
AutoFlush = true
|
||||
};
|
||||
|
||||
IncludeTimestamp = includeTimestamp;
|
||||
}
|
||||
|
||||
public bool IncludeTimestamp { get; set; }
|
||||
|
||||
public void Write(in LogEntry entry)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (IncludeTimestamp)
|
||||
{
|
||||
_writer.Write('[');
|
||||
_writer.Write(entry.Timestamp.ToString("yyyy-MM-dd HH:mm:ss.fff"));
|
||||
_writer.Write(']');
|
||||
}
|
||||
|
||||
_writer.Write('[');
|
||||
_writer.Write(ToLevelLabel(entry.Level));
|
||||
_writer.Write(']');
|
||||
_writer.Write('[');
|
||||
_writer.Write(entry.Category);
|
||||
_writer.Write(']');
|
||||
_writer.Write(' ');
|
||||
|
||||
_writer.Write(entry.SourceFileName);
|
||||
if (entry.SourceLine > 0)
|
||||
{
|
||||
_writer.Write(':');
|
||||
_writer.Write(entry.SourceLine);
|
||||
}
|
||||
|
||||
_writer.Write(' ');
|
||||
_writer.WriteLine(entry.Message);
|
||||
|
||||
if (entry.Exception is not null)
|
||||
{
|
||||
_writer.WriteLine(entry.Exception);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
_writer.Flush();
|
||||
_writer.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static string ToLevelLabel(LogLevel level) => level switch
|
||||
{
|
||||
LogLevel.Trace => "TRACE",
|
||||
LogLevel.Debug => "DEBUG",
|
||||
LogLevel.Info => "INFO",
|
||||
LogLevel.Warning => "WARNING",
|
||||
LogLevel.Error => "ERROR",
|
||||
LogLevel.Critical => "CRITICAL",
|
||||
_ => "LOG",
|
||||
};
|
||||
}
|
||||
@@ -4,4 +4,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
@@ -12,9 +12,7 @@ public static class SharpEmuLog
|
||||
new(StringComparer.Ordinal);
|
||||
private static readonly object ConfigurationSync = new();
|
||||
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
|
||||
private static ISharpEmuLogSink _sink = new ConsoleLogSink(
|
||||
useColors: ResolveColorEnabledFromEnvironment(),
|
||||
includeTimestamp: false);
|
||||
private static ISharpEmuLogSink _sink = ResolveSinkFromEnvironment();
|
||||
|
||||
public static LogLevel MinimumLevel
|
||||
{
|
||||
@@ -37,11 +35,26 @@ public static class SharpEmuLog
|
||||
ArgumentNullException.ThrowIfNull(value);
|
||||
lock (ConfigurationSync)
|
||||
{
|
||||
if (ReferenceEquals(_sink, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_sink is IDisposable disposable)
|
||||
{
|
||||
try
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
|
||||
_sink = value;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void Configure(LogLevel? minimumLevel = null, ISharpEmuLogSink? sink = null)
|
||||
{
|
||||
if (minimumLevel.HasValue)
|
||||
@@ -55,6 +68,22 @@ public static class SharpEmuLog
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Disposes the active sink if it implements <see cref="IDisposable"/>.
|
||||
/// Call at shutdown to flush file buffers. Logging after this call
|
||||
/// continues to work for non-disposable sinks (e.g. <see cref="ConsoleLogSink"/>).
|
||||
/// </summary>
|
||||
public static void Shutdown()
|
||||
{
|
||||
lock (ConfigurationSync)
|
||||
{
|
||||
if (_sink is IDisposable disposable)
|
||||
{
|
||||
disposable.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static SharpEmuLogger For(string category)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(category);
|
||||
@@ -146,6 +175,30 @@ public static class SharpEmuLog
|
||||
return !IsTrueLike(raw);
|
||||
}
|
||||
|
||||
private static ISharpEmuLogSink ResolveSinkFromEnvironment()
|
||||
{
|
||||
var consoleSink = new ConsoleLogSink(
|
||||
useColors: ResolveColorEnabledFromEnvironment(),
|
||||
includeTimestamp: false);
|
||||
|
||||
var logFilePath = Environment.GetEnvironmentVariable("SHARPEMU_LOG_FILE");
|
||||
if (!string.IsNullOrWhiteSpace(logFilePath))
|
||||
{
|
||||
try
|
||||
{
|
||||
var fileSink = new FileLogSink(logFilePath, append: true, includeTimestamp: true);
|
||||
return new CompositeLogSink(consoleSink, fileSink);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// Bootstrapping — the logging system is not yet active, so stderr is the only channel.
|
||||
Console.Error.WriteLine($"[SHARPEMU_LOG] Failed to open log file '{logFilePath}': {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
return consoleSink;
|
||||
}
|
||||
|
||||
private static bool IsTrueLike(string? text)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(text))
|
||||
|
||||
Reference in New Issue
Block a user