Compare commits

..

3 Commits

Author SHA1 Message Date
ParantezTech e5080f2792 Merge branch 'main' into savedata-mount3 2026-07-03 13:23:07 +03:00
Berk 92526ecacf [core] Update native execution and kernel exports, phtread improvement (#13) 2026-07-03 13:19:24 +03:00
ParantezTech f3fa0cb517 [saveData] Add support for sceSaveDataMount3 2026-07-03 12:50:33 +03:00
93 changed files with 1927 additions and 25597 deletions
+1 -1
View File
@@ -14,7 +14,7 @@ indent_size = 4
indent_style = space
tab_width = 4
[*.{md,json,yml,xml,props,csproj}]
[*.{md,json,yml,props,csproj}]
indent_size = 2
tab_width = 2
Binary file not shown.

Before

Width:  |  Height:  |  Size: 120 KiB

Before

Width:  |  Height:  |  Size: 1.1 MiB

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

+1 -1
View File
@@ -126,7 +126,7 @@ jobs:
needs:
- init
- build
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
runs-on: ubuntu-latest
permissions:
contents: write
-3
View File
@@ -26,7 +26,6 @@ arm64/
*.userosscache
*.sln.docstates
.packages
packages/
*.nupkg
.nuget/
@@ -37,5 +36,3 @@ packages/
Thumbs.db
Desktop.ini
ehthumbs.db
.vs/
+3 -9
View File
@@ -1,22 +1,16 @@
<!--
<!--
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>
BIN
View File
Binary file not shown.
+1 -3
View File
@@ -3,14 +3,12 @@ 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/**",
"assets/images/**"
".github/images/**"
]
precedence = "aggregate"
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
-1
View File
@@ -7,7 +7,6 @@ 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.

Before

Width:  |  Height:  |  Size: 92 KiB

+1 -1
View File
@@ -1,6 +1,6 @@
{
"sdk": {
"version": "10.0.103",
"rollForward": "latestFeature"
"rollForward": "disable"
}
}
-18
View File
@@ -1,18 +0,0 @@
<?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>
+2 -115
View File
@@ -3,7 +3,6 @@
using SharpEmu.Core.Runtime;
using SharpEmu.Core.Cpu;
using SharpEmu.GUI;
using SharpEmu.HLE;
using SharpEmu.Logging;
using System.Runtime.InteropServices;
@@ -25,44 +24,12 @@ 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;
@@ -78,7 +45,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}");
@@ -134,58 +101,6 @@ 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;
@@ -755,32 +670,4 @@ 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);
}
+1 -26
View File
@@ -7,14 +7,11 @@ 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>
<!-- 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>
<OutputType>Exe</OutputType>
<AssemblyName>SharpEmu</AssemblyName>
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
<SelfContained>true</SelfContained>
@@ -33,15 +30,6 @@ 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>
@@ -50,17 +38,4 @@ 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 -342
View File
@@ -4,108 +4,9 @@
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"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=="
"requested": "[10.0.3, )",
"resolved": "10.0.3",
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
},
"Microsoft.DotNet.PlatformAbstractions": {
"type": "Transitive",
@@ -158,38 +59,6 @@
"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",
@@ -204,16 +73,6 @@
"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"
},
@@ -222,7 +81,6 @@
"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, )"
}
@@ -230,48 +88,6 @@
"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, )",
@@ -287,16 +103,6 @@
"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, )",
@@ -316,61 +122,9 @@
"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",
@@ -378,52 +132,6 @@
}
},
"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",
@@ -431,52 +139,6 @@
}
},
"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",
@@ -484,4 +146,4 @@
}
}
}
}
}
+19 -13
View File
@@ -7,14 +7,11 @@ 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,
@@ -83,8 +80,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
string processImageName = "eboot.bin",
CpuExecutionOptions executionOptions = default)
{
Log.Debug("=== DispatchEntry START ===");
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
try
{
@@ -92,7 +89,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
}
catch (Exception ex)
{
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
throw;
}
}
@@ -105,8 +103,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
string moduleName = "module",
CpuExecutionOptions executionOptions = default)
{
Log.Debug("=== DispatchModuleInitializer START ===");
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
try
{
@@ -121,7 +119,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
}
catch (Exception ex)
{
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
throw;
}
}
@@ -135,7 +134,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
CpuExecutionOptions executionOptions = default,
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
{
Log.Debug("DispatchEntryCore STARTING...");
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
LastEntryPoint = entryPoint;
LastTrapInfo = null;
@@ -307,7 +306,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
LastMilestoneLog,
Environment.NewLine,
$"CpuEngine native-only failed: {backendError}");
Log.Error($"Native backend FAILED: {backendError}");
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
return FailEarly(
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
CpuExitReason.NativeBackendUnavailable);
@@ -416,8 +415,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
const ulong entryParamsSize = 0x20;
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
@@ -457,6 +456,13 @@ 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,15 +338,10 @@ public sealed partial class DirectExecutionBackend
{
orbisGen2Result = DispatchBootstrapBridge();
}
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, 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)
{
@@ -358,21 +353,12 @@ public sealed partial class DirectExecutionBackend
}
orbisGen2Result = (OrbisGen2Result)returnValue;
}
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);
}
else
{
dispatchResolved = false;
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
}
}
finally
{
@@ -642,56 +628,25 @@ public sealed partial class DirectExecutionBackend
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
"Q2V+iqvjgC0" or // vsnprintf
"q1cHNfGycLI" or // scePadRead
"xk0AcarP3V4" or // scePadOpen
"yH17Q6NWtVg" or // sceUserServiceGetEvent
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
"K-jXhbt2gn4"; // scePthreadMutexTrylock
"qvMUCyyaCSI"; // sceKernelAprSubmitCommandBufferAndGetId
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;
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)
if (!expectedFileProbeMiss && !expectedTimedWaitTimeout && !expectedMutexTrylockBusy)
{
return true;
}
if (!ShouldLogExpectedImportResults())
{
return false;
}
var key = nid + "\0" + resultValue;
var key = nid + "\0" + (int)result;
int count;
lock (_importResultLogSampleGate)
{
@@ -703,12 +658,6 @@ 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
@@ -1231,11 +1180,8 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
return OrbisGen2Result.ORBIS_GEN2_OK;
}
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
if (!TryResolveRuntimeSymbolAddress(symbolName, out var 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;
}
@@ -1248,62 +1194,6 @@ 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;
@@ -1679,14 +1679,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[num++] = 65;
ptr2[num++] = 95;
ptr2[num++] = 195;
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;
uint num2 = default(uint);
VirtualProtect(ptr, 192u, 32u, &num2);
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
return (nint)ptr;
}
catch
{
@@ -1767,11 +1763,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
tlsHandlerAddress[num++] = 195;
_tlsPatchStubOffset = (num + 15) & ~15;
uint num2 = default(uint);
if (!VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS handler at 0x{_tlsHandlerAddress:X16}");
return;
}
VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2);
FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, TlsHandlerRegionSize);
Console.Error.WriteLine($"[LOADER][INFO] TLS handler at 0x{_tlsHandlerAddress:X16}");
}
@@ -1792,11 +1784,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr2[i] = 144;
}
uint num = default(uint);
if (!VirtualProtect(ptr, 4096u, 32u, &num))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for unresolved return stub at 0x{(nint)ptr:X16}");
return 0;
}
VirtualProtect(ptr, 4096u, 32u, &num);
FlushInstructionCache(GetCurrentProcess(), ptr, 16u);
return (nint)ptr;
}
@@ -1843,11 +1831,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
EmitByte(code, ref offset, 0xC3);
uint oldProtect = default;
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for guest return stub at 0x{(nint)ptr:X16}");
return 0;
}
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
return (nint)ptr;
}
@@ -1939,11 +1923,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
uint oldProtect = default;
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
return 0;
}
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
return (nint)ptr;
}
@@ -2274,11 +2254,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
ptr[num2++] = 144;
}
uint flNewProtect = default(uint);
if (!VirtualProtect((void*)num, 32u, 32u, &flNewProtect))
{
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS store helper at 0x{num:X16}");
return 0;
}
VirtualProtect((void*)num, 32u, 32u, &flNewProtect);
FlushInstructionCache(GetCurrentProcess(), (void*)num, 32u);
return num;
}
@@ -3615,11 +3591,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return GuestNativeCallExitReason.Exception;
}
uint oldProtect = default(uint);
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
{
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
return GuestNativeCallExitReason.Exception;
}
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
ActiveGuestThreadYieldRequested = false;
@@ -3756,11 +3728,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return GuestNativeCallExitReason.Exception;
}
uint oldProtect = default(uint);
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
{
reason = $"VirtualProtect failed for guest continuation stub at 0x{(nint)ptr:X16}";
return GuestNativeCallExitReason.Exception;
}
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
ActiveGuestThreadYieldRequested = false;
@@ -4020,12 +3988,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return false;
}
uint num5 = default(uint);
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;
}
VirtualProtect(ptr, stubSize, 64u, &num5);
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
if (_hostRspSlotStorage != 0)
{
+85 -199
View File
@@ -4,19 +4,14 @@
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;
@@ -24,8 +19,7 @@ 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 FullCommitRegionLimit = 4UL << 30;
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
private const uint MEM_COMMIT = 0x1000;
@@ -41,7 +35,6 @@ 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);
@@ -58,11 +51,7 @@ 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* GetCurrentProcess();
[DllImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
{
@@ -92,7 +81,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterWriteLock();
try
{
InsertRegionSorted(new MemoryRegion
_regions.Add(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
@@ -107,7 +96,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
var allocationKind = executable ? "executable memory" : "data memory";
TraceVmem($"Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
return true;
}
@@ -121,9 +110,7 @@ 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 &&
alignedSize > FullCommitRegionLimit;
var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold;
void* result = null;
if (preferReserveOnly)
@@ -152,7 +139,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
throw new InvalidOperationException($"Failed to allocate exact mapping at 0x{desiredAddress:X16} ({alignedSize} bytes)");
}
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
Console.Error.WriteLine($"[VMEM] Could not allocate at 0x{desiredAddress:X16}, trying any address...");
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
if (result == null)
@@ -206,12 +193,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
lazyPrimeState = committedBytes == primeBytes
? $"ok:{committedBytes:X}"
: $"partial:{committedBytes:X}/{primeBytes:X}";
TraceVmem($"Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
Console.Error.WriteLine($"[VMEM] Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
}
else
{
lazyPrimeState = $"fail:{primeBytes:X}";
TraceVmem($"Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
Console.Error.WriteLine($"[VMEM] Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
}
}
else
@@ -223,7 +210,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterWriteLock();
try
{
InsertRegionSorted(new MemoryRegion
_regions.Add(new MemoryRegion
{
VirtualAddress = actualAddress,
Size = alignedSize,
@@ -240,7 +227,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var allocationKind = reservedOnly
? "reserved data memory (lazy commit)"
: (executable ? "executable memory" : "data memory");
TraceVmem($"Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
Console.Error.WriteLine($"[VMEM] Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
return actualAddress;
}
@@ -260,8 +247,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
var alignedSize = AlignUp(size, PageSize);
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
var cursor = AlignUp(desiredAddress, effectiveAlignment);
for (var attempt = 0; attempt < 0x10000; attempt++)
{
@@ -281,7 +267,6 @@ 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;
}
@@ -349,10 +334,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
}
_regions.Clear();
_pageProtections.Clear();
lock (_allocationSearchHintGate)
{
_allocationSearchHints.Clear();
}
}
finally
{
@@ -409,7 +390,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
ApplySegmentProtection(mapStart, mapEnd, protection);
TraceVmem($"Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
}
finally
{
@@ -458,7 +439,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
if ((flags & ProgramHeaderFlags.Execute) != 0)
{
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
FlushInstructionCache(null, (void*)address, (nuint)size);
}
}
@@ -492,34 +473,30 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock();
try
{
var region = FindRegion(virtualAddress, (ulong)destination.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)destination.Length,
region,
out var offset))
foreach (var region in _regions)
{
var srcPtr = (void*)(region.VirtualAddress + offset);
if (destination.IsEmpty)
if (TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
{
return true;
}
if (region.IsReservedOnly)
{
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
var srcPtr = (void*)(region.VirtualAddress + offset);
if (destination.IsEmpty)
{
return false;
return true;
}
if (region.IsReservedOnly)
{
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
return false;
}
}
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
{
requiresExclusiveAccess = true;
break;
}
}
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);
@@ -556,34 +533,30 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock();
try
{
var region = FindRegion(virtualAddress, (ulong)source.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)source.Length,
region,
out var offset))
foreach (var region in _regions)
{
var destPtr = (void*)(region.VirtualAddress + offset);
if (source.IsEmpty)
if (TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
{
return true;
}
if (region.IsReservedOnly)
{
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
var destPtr = (void*)(region.VirtualAddress + offset);
if (source.IsEmpty)
{
return false;
return true;
}
if (region.IsReservedOnly)
{
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
return false;
}
}
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
{
requiresExclusiveAccess = true;
break;
}
}
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);
@@ -616,14 +589,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
{
var region = FindRegion(virtualAddress, (ulong)destination.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)destination.Length,
region,
out var offset))
foreach (var region in _regions)
{
if (!TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
{
continue;
}
var srcPtr = (void*)(region.VirtualAddress + offset);
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
{
@@ -665,14 +637,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
{
var region = FindRegion(virtualAddress, (ulong)source.Length);
if (region is not null &&
TryResolveRegionOffset(
virtualAddress,
(ulong)source.Length,
region,
out var offset))
foreach (var region in _regions)
{
if (!TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
{
continue;
}
var destPtr = (void*)(region.VirtualAddress + offset);
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
{
@@ -706,7 +677,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
if (IsExecutableProtection(oldProtect))
{
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
FlushInstructionCache(null, destPtr, (nuint)source.Length);
}
}
@@ -728,9 +699,15 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock();
try
{
return FindRegion(virtualAddress, 1) is not null
? (void*)virtualAddress
: null;
foreach (var region in _regions)
{
if (virtualAddress >= region.VirtualAddress &&
virtualAddress < region.VirtualAddress + region.Size)
{
return (void*)virtualAddress;
}
}
return null;
}
finally
{
@@ -743,7 +720,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
_gate.EnterReadLock();
try
{
return FindRegion(virtualAddress, size) is not null;
foreach (var region in _regions)
{
if (TryResolveRegionOffset(virtualAddress, size, region, out _))
{
return true;
}
}
return false;
}
finally
{
@@ -753,48 +737,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
private MemoryRegion? FindRegion(ulong address, ulong size)
{
var low = 0;
var high = _regions.Count - 1;
MemoryRegion? candidate = null;
while (low <= high)
foreach (var region in _regions)
{
var middle = low + ((high - low) >> 1);
var region = _regions[middle];
if (region.VirtualAddress <= address)
if (TryResolveRegionOffset(address, size, region, out _))
{
candidate = region;
low = middle + 1;
}
else
{
high = middle - 1;
return region;
}
}
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);
return null;
}
private bool TryGetOverlappingRegionEnd(ulong address, ulong size, out ulong overlapEnd)
@@ -812,16 +762,6 @@ 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);
@@ -836,37 +776,6 @@ 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;
@@ -1035,29 +944,6 @@ 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)
+37 -42
View File
@@ -11,7 +11,6 @@ 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;
@@ -21,8 +20,6 @@ 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)
@@ -131,7 +128,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
var normalizedEbootPath = Path.GetFullPath(ebootPath);
using var app0Binding = BindApp0Root(normalizedEbootPath);
Log.Info($"Loading: {ebootPath}");
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
LastExecutionDiagnostics = null;
LastExecutionTrace = null;
LastSessionSummary = null;
@@ -143,7 +140,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
Console.Error.WriteLine($"[RUNTIME] 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);
@@ -166,7 +163,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
processImageName);
if (initializerResult is { } failedInitializerResult)
{
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
@@ -174,8 +171,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return failedInitializerResult;
}
Log.Info($"Dispatching, gen: {generation}");
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
var result = _cpuDispatcher.DispatchEntry(
image.EntryPoint,
@@ -185,8 +182,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
processImageName,
_cpuExecutionOptions);
Log.Info($"DispatchEntry returned: {result}");
Log.Info($"Dispatch result: {result}");
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
@@ -437,8 +434,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
moduleName = $"module#{i}";
}
Log.Info(
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
var result = _cpuDispatcher.DispatchModuleInitializer(
initEntryPoint,
@@ -449,8 +446,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
_cpuExecutionOptions);
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
{
Log.Error(
$"Module start failed: {moduleName} -> {result}");
Console.Error.WriteLine(
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
return result;
}
}
@@ -471,8 +468,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return null;
}
Log.Info(
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
Console.Error.WriteLine(
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
var result = RunInitializerList(
$"{label}:preinit",
@@ -511,8 +508,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
continue;
}
Log.Debug(
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
var result = _cpuDispatcher.DispatchEntry(
initializerAddress,
@@ -546,7 +543,6 @@ 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)
@@ -558,9 +554,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
}
var allModulePaths = moduleDirectories
.SelectMany(directory => Directory
.EnumerateFiles(directory)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
.SelectMany(Directory.EnumerateFiles)
.Where(path =>
{
var extension = Path.GetExtension(path);
@@ -568,6 +562,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
})
.Distinct(StringComparer.OrdinalIgnoreCase)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.ToArray();
var modulePaths = allModulePaths
@@ -580,7 +575,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
.ToArray();
if (skippedModules.Length > 0)
{
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
}
if (modulePaths.Length == 0)
@@ -588,8 +583,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return loadedImages;
}
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
Log.Info($"Loading {modulePaths.Length} module(s)...");
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
var loadedModules = 0;
var failedModules = 0;
var mergedImportCount = 0;
@@ -624,18 +619,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
loadedModules++;
Log.Info(
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
Console.Error.WriteLine(
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
}
catch (Exception ex)
{
failedModules++;
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
}
}
Log.Info(
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
Console.Error.WriteLine(
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
return loadedImages;
}
@@ -655,8 +650,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
if (rebound != 0 || unresolved != 0)
{
Log.Info(
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
}
}
@@ -688,8 +683,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (logRebind)
{
Log.Warning(
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
}
unresolved++;
@@ -701,8 +696,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (logRebind)
{
Log.Error(
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
}
unresolved++;
@@ -711,8 +706,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
if (logRebind)
{
Log.Debug(
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
Console.Error.WriteLine(
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
}
rebound++;
@@ -748,8 +743,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
{
Log.Warning(
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
Console.Error.WriteLine(
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
}
continue;
@@ -858,8 +853,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
image.EntryPoint,
isMain,
isSystemModule);
Log.Info(
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
Console.Error.WriteLine(
$"[RUNTIME] 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 -5
View File
@@ -1,4 +1,4 @@
<!--
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
@@ -14,10 +14,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PackageReference Include="Iced" />
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
-11
View File
@@ -72,7 +72,6 @@
"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, )"
}
@@ -89,16 +88,6 @@
"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, )",
-198
View File
@@ -1,198 +0,0 @@
<!--
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>
-26
View File
@@ -1,26 +0,0 @@
// 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();
}
}
-645
View File
@@ -1,645 +0,0 @@
// 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);
}
-156
View File
@@ -1,156 +0,0 @@
// 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",
};
}
}
-47
View File
@@ -1,47 +0,0 @@
// 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.
}
}
}
-67
View File
@@ -1,67 +0,0 @@
// 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.
}
}
}
-8
View File
@@ -1,8 +0,0 @@
// 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);
-265
View File
@@ -1,265 +0,0 @@
<!--
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
-35
View File
@@ -1,35 +0,0 @@
<!--
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>
-186
View File
@@ -1,186 +0,0 @@
{
"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=="
}
}
}
}
+9 -135
View File
@@ -2,20 +2,25 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Text;
namespace SharpEmu.HLE;
public sealed class CpuContext(ICpuMemory memory, Generation generation)
public sealed class CpuContext
{
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 ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory));
public CpuContext(ICpuMemory memory, Generation generation)
{
Memory = memory ?? throw new ArgumentNullException(nameof(memory));
TargetGeneration = generation;
}
public Generation TargetGeneration { get; } = generation;
public ICpuMemory Memory { get; }
public Generation TargetGeneration { get; }
public ulong Rip { get; set; }
@@ -125,91 +130,6 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
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)];
@@ -230,33 +150,6 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
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];
@@ -276,23 +169,4 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
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;
}
}
+2 -6
View File
@@ -47,7 +47,6 @@ 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;
}
@@ -106,7 +105,6 @@ 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;
@@ -114,13 +112,11 @@ public sealed class ModuleManager : IModuleManager
if ((export.Target & context.TargetGeneration) == 0)
{
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;
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
return false;
}
context.ClearRaxWriteFlag();
int ret = ((SysAbiFunction)function).Invoke(context);
-4
View File
@@ -8,10 +8,6 @@ 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>
File diff suppressed because it is too large Load Diff
-313
View File
@@ -1,313 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
internal enum Gen5ShaderEncoding
{
Sop1,
Sop2,
Sopc,
Sopp,
Sopk,
Smrd,
Smem,
Mubuf,
Mtbuf,
Vop1,
Vop2,
Vopc,
Vop3,
Vintrp,
Ds,
Flat,
Vop3p,
Mimg,
Exp,
}
internal enum Gen5OperandKind
{
ScalarRegister,
VectorRegister,
EncodedConstant,
LiteralConstant,
}
internal enum Gen5ShaderResourceKind
{
ReadOnlyTexture,
ReadWriteTexture,
Sampler,
ConstantBuffer,
}
internal enum Gen5PixelOutputKind
{
Float,
Uint,
Sint,
}
internal enum Gen5SpirvStage
{
Vertex,
Pixel,
Compute,
}
internal sealed record Gen5SpirvShader(
byte[] Spirv,
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
IReadOnlyList<Gen5ImageBinding> ImageBindings,
uint AttributeCount,
IReadOnlyList<Gen5VertexInputBinding> VertexInputs);
internal readonly record struct Gen5ShaderResourceMapping(
Gen5ShaderResourceKind Kind,
uint Slot,
uint OffsetDwords,
bool SizeFlag);
internal sealed record Gen5ShaderMetadata(
uint ExtendedUserDataSizeDwords,
uint ShaderResourceTableSizeDwords,
IReadOnlyDictionary<uint, uint> DirectResources,
IReadOnlyList<Gen5ShaderResourceMapping> Resources);
internal readonly record struct Gen5ComputeSystemRegisters(
uint? WorkGroupXRegister,
uint? WorkGroupYRegister,
uint? WorkGroupZRegister,
uint? ThreadGroupSizeRegister)
{
public bool TryGetExpression(uint scalarRegister, out string expression)
{
if (WorkGroupXRegister == scalarRegister)
{
expression = "gl_WorkGroupID.x";
return true;
}
if (WorkGroupYRegister == scalarRegister)
{
expression = "gl_WorkGroupID.y";
return true;
}
if (WorkGroupZRegister == scalarRegister)
{
expression = "gl_WorkGroupID.z";
return true;
}
if (ThreadGroupSizeRegister == scalarRegister)
{
expression = "(gl_WorkGroupSize.x * gl_WorkGroupSize.y * gl_WorkGroupSize.z)";
return true;
}
expression = string.Empty;
return false;
}
public void ClearStaticValues(Span<uint> scalarRegisters)
{
ClearStaticValue(scalarRegisters, WorkGroupXRegister);
ClearStaticValue(scalarRegisters, WorkGroupYRegister);
ClearStaticValue(scalarRegisters, WorkGroupZRegister);
ClearStaticValue(scalarRegisters, ThreadGroupSizeRegister);
}
private static void ClearStaticValue(Span<uint> scalarRegisters, uint? scalarRegister)
{
if (scalarRegister is { } register && register < scalarRegisters.Length)
{
scalarRegisters[(int)register] = 0;
}
}
}
internal sealed record Gen5ShaderState(
Gen5ShaderProgram Program,
IReadOnlyList<uint> UserData,
Gen5ShaderMetadata? Metadata,
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
uint UserDataScalarRegisterBase = 0);
internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
{
public static Gen5Operand Scalar(uint index) =>
new(Gen5OperandKind.ScalarRegister, index);
public static Gen5Operand Vector(uint index) =>
new(Gen5OperandKind.VectorRegister, index);
public static Gen5Operand Source(uint encoded, uint? literal = null)
{
if (encoded >= 256)
{
return Vector(encoded - 256);
}
if (encoded is 249 or 255 && literal.HasValue)
{
return new(Gen5OperandKind.LiteralConstant, literal.Value);
}
if (encoded <= 105 || encoded is 106 or 107 or 124 or 126 or 127)
{
return Scalar(encoded);
}
return new(Gen5OperandKind.EncodedConstant, encoded);
}
public override string ToString() => Kind switch
{
Gen5OperandKind.ScalarRegister => $"s{Value}",
Gen5OperandKind.VectorRegister => $"v{Value}",
Gen5OperandKind.LiteralConstant => $"0x{Value:X8}",
_ => $"src[{Value}]",
};
}
internal abstract record Gen5InstructionControl;
internal sealed record Gen5ImageControl(
uint Dmask,
uint VectorAddress,
IReadOnlyList<uint> AddressRegisters,
uint VectorData,
uint ScalarResource,
uint ScalarSampler,
uint Dimension,
bool IsArray,
bool Glc,
bool Slc) : Gen5InstructionControl
{
public uint GetAddressRegister(int component) =>
component < AddressRegisters.Count
? AddressRegisters[component]
: VectorAddress + (uint)component;
}
internal sealed record Gen5GlobalMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
uint ScalarAddress,
int OffsetBytes,
bool Glc,
bool Slc) : Gen5InstructionControl;
internal sealed record Gen5BufferMemoryControl(
uint DwordCount,
uint VectorAddress,
uint VectorData,
uint ScalarResource,
int OffsetBytes,
bool IndexEnabled,
bool OffsetEnabled,
bool Glc,
bool Slc) : Gen5InstructionControl;
internal sealed record Gen5ExportControl(
uint Target,
uint EnableMask,
bool Compressed,
bool Done,
bool ValidMask) : Gen5InstructionControl;
internal sealed record Gen5InterpolationControl(
uint Attribute,
uint Channel) : Gen5InstructionControl;
internal sealed record Gen5Vop3Control(
uint AbsoluteMask,
uint NegateMask,
uint OutputModifier,
bool Clamp,
uint? ScalarDestination) : Gen5InstructionControl;
internal sealed record Gen5SdwaControl(
uint DestinationSelect,
uint Source0Select,
uint Source1Select,
uint AbsoluteMask,
uint NegateMask,
uint OutputModifier,
bool Clamp) : Gen5InstructionControl;
internal sealed record Gen5DppControl(
uint Control,
bool FetchInactive,
bool BoundControl,
uint AbsoluteMask,
uint NegateMask,
uint BankMask,
uint RowMask) : Gen5InstructionControl;
internal sealed record Gen5ScalarMemoryControl(
uint DestinationCount,
int ImmediateOffsetBytes,
uint? DynamicOffsetRegister) : Gen5InstructionControl;
internal sealed record Gen5DataShareControl(
uint Offset0,
uint Offset1,
bool Gds) : Gen5InstructionControl;
internal sealed record Gen5ImageBinding(
uint Pc,
string Opcode,
Gen5ImageControl Control,
IReadOnlyList<uint> ResourceDescriptor,
IReadOnlyList<uint> SamplerDescriptor,
uint? MipLevel);
internal sealed record Gen5GlobalMemoryBinding(
uint ScalarAddress,
ulong BaseAddress,
IReadOnlyList<uint> InstructionPcs,
byte[] Data);
internal sealed record Gen5VertexInputBinding(
uint Pc,
uint Location,
uint ComponentCount,
uint DataFormat,
uint NumberFormat,
ulong BaseAddress,
uint Stride,
uint OffsetBytes,
byte[] Data);
internal sealed record Gen5ShaderEvaluation(
IReadOnlyList<uint> InitialScalarRegisters,
IReadOnlyList<uint> ScalarRegisters,
IReadOnlyDictionary<uint, IReadOnlyList<uint>> ScalarRegistersByPc,
IReadOnlyList<Gen5ImageBinding> ImageBindings,
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
IReadOnlySet<uint>? RuntimeScalarRegisters = null,
IReadOnlyList<Gen5VertexInputBinding>? VertexInputs = null);
internal sealed record Gen5ShaderInstruction(
uint Pc,
Gen5ShaderEncoding Encoding,
string Opcode,
IReadOnlyList<uint> Words,
IReadOnlyList<Gen5Operand> Sources,
IReadOnlyList<Gen5Operand> Destinations,
Gen5InstructionControl? Control);
internal sealed record Gen5ShaderProgram(
ulong Address,
IReadOnlyList<Gen5ShaderInstruction> Instructions)
{
public IEnumerable<Gen5ImageControl> ImageResources =>
Instructions
.Select(instruction => instruction.Control)
.OfType<Gen5ImageControl>();
}
@@ -1,124 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Agc;
internal static class Gen5ShaderMetadataReader
{
private const ulong ShaderUserDataOffset = 0x08;
private const int ResourceClassCount = 4;
private const int MaxMetadataEntries = 4096;
public static bool TryRead(
CpuContext ctx,
ulong shaderHeaderAddress,
out Gen5ShaderMetadata metadata)
{
metadata = default!;
if (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
userDataAddress == 0 ||
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
{
return false;
}
var resourceOffsets = new ulong[ResourceClassCount];
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{
if (!ctx.TryReadUInt64(
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
out resourceOffsets[resourceClass]))
{
return false;
}
}
if (!ctx.TryReadUInt16(userDataAddress + 0x28, out var extendedUserDataSize) ||
!ctx.TryReadUInt16(userDataAddress + 0x2A, out var shaderResourceTableSize) ||
!ctx.TryReadUInt16(userDataAddress + 0x2C, out var directResourceCount) ||
directResourceCount > MaxMetadataEntries)
{
return false;
}
var resourceCounts = new ushort[ResourceClassCount];
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{
if (!ctx.TryReadUInt16(
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
out resourceCounts[resourceClass]) ||
resourceCounts[resourceClass] > MaxMetadataEntries)
{
return false;
}
}
var directResources = new Dictionary<uint, uint>();
if (directResourceCount != 0)
{
if (directResourceOffsetsAddress == 0)
{
return false;
}
for (uint type = 0; type < directResourceCount; type++)
{
if (!ctx.TryReadUInt16(directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
{
return false;
}
if (offset != ushort.MaxValue)
{
directResources[type] = offset;
}
}
}
var resources = new List<Gen5ShaderResourceMapping>();
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
{
var count = resourceCounts[resourceClass];
if (count == 0)
{
continue;
}
if (resourceOffsets[resourceClass] == 0)
{
return false;
}
for (uint slot = 0; slot < count; slot++)
{
if (!ctx.TryReadUInt16(
resourceOffsets[resourceClass] + slot * sizeof(ushort),
out var sharp))
{
return false;
}
var offset = (uint)(sharp & 0x7FFF);
if (offset == 0x7FFF)
{
continue;
}
resources.Add(new Gen5ShaderResourceMapping(
(Gen5ShaderResourceKind)resourceClass,
slot,
offset,
(sharp & 0x8000) != 0));
}
}
metadata = new Gen5ShaderMetadata(
extendedUserDataSize,
shaderResourceTableSize,
directResources,
resources);
return true;
}
}
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
-167
View File
@@ -1,167 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Libs.Agc;
internal static class SpirvFixedShaders
{
public static byte[] CreateFullscreenVertex(uint attributeCount)
{
var module = new SpirvModuleBuilder();
module.AddCapability(SpirvCapability.Shader);
var voidType = module.TypeVoid();
var boolType = module.TypeBool();
var uintType = module.TypeInt(32, signed: false);
var floatType = module.TypeFloat(32);
var vec4Type = module.TypeVector(floatType, 4);
var inputUintPointer = module.TypePointer(SpirvStorageClass.Input, uintType);
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
var vertexIndex = module.AddGlobalVariable(inputUintPointer, SpirvStorageClass.Input);
module.AddName(vertexIndex, "vertexIndex");
module.AddDecoration(
vertexIndex,
SpirvDecoration.BuiltIn,
(uint)SpirvBuiltIn.VertexIndex);
var position = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
module.AddName(position, "position");
module.AddDecoration(position, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Position);
var attributes = new uint[attributeCount];
for (uint index = 0; index < attributeCount; index++)
{
attributes[index] =
module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
module.AddName(attributes[index], $"attr{index}");
module.AddDecoration(attributes[index], SpirvDecoration.Location, index);
module.AddDecoration(attributes[index], SpirvDecoration.NoPerspective);
}
var functionType = module.TypeFunction(voidType);
var main = module.BeginFunction(voidType, functionType);
module.AddName(main, "main");
module.AddLabel();
var indexValue = module.AddInstruction(SpirvOp.Load, uintType, vertexIndex);
var one = module.Constant(uintType, 1);
var two = module.Constant(uintType, 2);
var shifted = module.AddInstruction(SpirvOp.ShiftLeftLogical, uintType, indexValue, one);
var xBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, shifted, two);
var yBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, indexValue, two);
var x = module.AddInstruction(SpirvOp.ConvertUToF, floatType, xBits);
var y = module.AddInstruction(SpirvOp.ConvertUToF, floatType, yBits);
var zero = module.ConstantFloat(floatType, 0f);
var oneFloat = module.ConstantFloat(floatType, 1f);
var twoFloat = module.ConstantFloat(floatType, 2f);
var xPosition = module.AddInstruction(SpirvOp.FMul, floatType, x, twoFloat);
xPosition = module.AddInstruction(SpirvOp.FSub, floatType, xPosition, oneFloat);
var yPosition = module.AddInstruction(SpirvOp.FMul, floatType, y, twoFloat);
yPosition = module.AddInstruction(SpirvOp.FSub, floatType, yPosition, oneFloat);
var positionValue = module.AddInstruction(
SpirvOp.CompositeConstruct,
vec4Type,
xPosition,
yPosition,
zero,
oneFloat);
module.AddStatement(SpirvOp.Store, position, positionValue);
var attributeValue = module.AddInstruction(
SpirvOp.CompositeConstruct,
vec4Type,
x,
y,
zero,
oneFloat);
foreach (var attribute in attributes)
{
module.AddStatement(SpirvOp.Store, attribute, attributeValue);
}
module.AddStatement(SpirvOp.Return);
module.EndFunction();
var interfaces = new uint[2 + attributes.Length];
interfaces[0] = vertexIndex;
interfaces[1] = position;
attributes.CopyTo(interfaces, 2);
module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main", interfaces);
_ = boolType;
return module.Build();
}
public static byte[] CreateCopyFragment()
{
var module = new SpirvModuleBuilder();
module.AddCapability(SpirvCapability.Shader);
var voidType = module.TypeVoid();
var floatType = module.TypeFloat(32);
var vec2Type = module.TypeVector(floatType, 2);
var vec4Type = module.TypeVector(floatType, 4);
var inputVec4Pointer = module.TypePointer(SpirvStorageClass.Input, vec4Type);
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
var imageType = module.TypeImage(
floatType,
SpirvImageDim.Dim2D,
depth: false,
arrayed: false,
multisampled: false,
sampled: 1,
SpirvImageFormat.Unknown);
var sampledImageType = module.TypeSampledImage(imageType);
var sampledImagePointer =
module.TypePointer(SpirvStorageClass.UniformConstant, sampledImageType);
var attribute = module.AddGlobalVariable(inputVec4Pointer, SpirvStorageClass.Input);
module.AddName(attribute, "attr0");
module.AddDecoration(attribute, SpirvDecoration.Location, 0);
var texture = module.AddGlobalVariable(
sampledImagePointer,
SpirvStorageClass.UniformConstant);
module.AddName(texture, "tex0");
module.AddDecoration(texture, SpirvDecoration.DescriptorSet, 0);
module.AddDecoration(texture, SpirvDecoration.Binding, 1);
var output = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
module.AddName(output, "outColor");
module.AddDecoration(output, SpirvDecoration.Location, 0);
var functionType = module.TypeFunction(voidType);
var main = module.BeginFunction(voidType, functionType);
module.AddName(main, "main");
module.AddLabel();
var attributeValue = module.AddInstruction(SpirvOp.Load, vec4Type, attribute);
var coordinates = module.AddInstruction(
SpirvOp.VectorShuffle,
vec2Type,
attributeValue,
attributeValue,
0,
1);
var sampledImage = module.AddInstruction(SpirvOp.Load, sampledImageType, texture);
var lod = module.ConstantFloat(floatType, 0f);
var color = module.AddInstruction(
SpirvOp.ImageSampleExplicitLod,
vec4Type,
sampledImage,
coordinates,
2,
lod);
module.AddStatement(SpirvOp.Store, output, color);
module.AddStatement(SpirvOp.Return);
module.EndFunction();
module.AddEntryPoint(
SpirvExecutionModel.Fragment,
main,
"main",
[attribute, texture, output]);
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
return module.Build();
}
}
-885
View File
@@ -1,885 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Text;
namespace SharpEmu.Libs.Agc;
internal enum SpirvOp : ushort
{
Nop = 0,
Name = 5,
Extension = 10,
ExtInstImport = 11,
ExtInst = 12,
MemoryModel = 14,
EntryPoint = 15,
ExecutionMode = 16,
Capability = 17,
TypeVoid = 19,
TypeBool = 20,
TypeInt = 21,
TypeFloat = 22,
TypeVector = 23,
TypeImage = 25,
TypeSampler = 26,
TypeSampledImage = 27,
TypeArray = 28,
TypeRuntimeArray = 29,
TypeStruct = 30,
TypePointer = 32,
TypeFunction = 33,
ConstantTrue = 41,
ConstantFalse = 42,
Constant = 43,
ConstantComposite = 44,
ConstantNull = 46,
Function = 54,
FunctionParameter = 55,
FunctionEnd = 56,
FunctionCall = 57,
Variable = 59,
Load = 61,
Store = 62,
AccessChain = 65,
ArrayLength = 68,
Decorate = 71,
VectorExtractDynamic = 77,
VectorInsertDynamic = 78,
VectorShuffle = 79,
CompositeConstruct = 80,
CompositeExtract = 81,
CompositeInsert = 82,
CopyObject = 83,
SampledImage = 86,
ImageSampleImplicitLod = 87,
ImageSampleExplicitLod = 88,
ImageSampleDrefImplicitLod = 89,
ImageSampleDrefExplicitLod = 90,
ImageFetch = 95,
ImageGather = 96,
ImageDrefGather = 97,
ImageRead = 98,
ImageWrite = 99,
Image = 100,
ImageQuerySizeLod = 103,
ImageQuerySize = 104,
ImageQueryLod = 105,
ImageQueryLevels = 106,
ImageQuerySamples = 107,
ConvertFToU = 109,
ConvertFToS = 110,
ConvertSToF = 111,
ConvertUToF = 112,
UConvert = 113,
SConvert = 114,
FConvert = 115,
Bitcast = 124,
SNegate = 126,
FNegate = 127,
IAdd = 128,
FAdd = 129,
ISub = 130,
FSub = 131,
IMul = 132,
FMul = 133,
UDiv = 134,
SDiv = 135,
FDiv = 136,
UMod = 137,
SRem = 138,
SMod = 139,
FRem = 140,
FMod = 141,
IAddCarry = 149,
ISubBorrow = 150,
UMulExtended = 151,
SMulExtended = 152,
Any = 154,
All = 155,
IsNan = 156,
IsInf = 157,
LogicalEqual = 164,
LogicalNotEqual = 165,
LogicalOr = 166,
LogicalAnd = 167,
LogicalNot = 168,
Select = 169,
IEqual = 170,
INotEqual = 171,
UGreaterThan = 172,
SGreaterThan = 173,
UGreaterThanEqual = 174,
SGreaterThanEqual = 175,
ULessThan = 176,
SLessThan = 177,
ULessThanEqual = 178,
SLessThanEqual = 179,
FOrdEqual = 180,
FUnordEqual = 181,
FOrdNotEqual = 182,
FUnordNotEqual = 183,
FOrdLessThan = 184,
FUnordLessThan = 185,
FOrdGreaterThan = 186,
FUnordGreaterThan = 187,
FOrdLessThanEqual = 188,
FUnordLessThanEqual = 189,
FOrdGreaterThanEqual = 190,
FUnordGreaterThanEqual = 191,
ShiftRightLogical = 194,
ShiftRightArithmetic = 195,
ShiftLeftLogical = 196,
BitwiseOr = 197,
BitwiseXor = 198,
BitwiseAnd = 199,
Not = 200,
BitFieldInsert = 201,
BitFieldSExtract = 202,
BitFieldUExtract = 203,
BitReverse = 204,
BitCount = 205,
ControlBarrier = 224,
MemoryBarrier = 225,
AtomicIAdd = 234,
Phi = 245,
LoopMerge = 246,
SelectionMerge = 247,
Label = 248,
Branch = 249,
BranchConditional = 250,
Switch = 251,
Kill = 252,
Return = 253,
ReturnValue = 254,
Unreachable = 255,
GroupNonUniformElect = 333,
GroupNonUniformAll = 334,
GroupNonUniformAny = 335,
GroupNonUniformAllEqual = 336,
GroupNonUniformBroadcast = 337,
GroupNonUniformBroadcastFirst = 338,
GroupNonUniformBallot = 339,
GroupNonUniformShuffle = 345,
GroupNonUniformShuffleXor = 346,
GroupNonUniformShuffleUp = 347,
GroupNonUniformShuffleDown = 348,
}
internal enum SpirvCapability : uint
{
Shader = 1,
Float16 = 9,
Float64 = 10,
Int64 = 11,
Int16 = 22,
ImageGatherExtended = 25,
StorageImageExtendedFormats = 49,
ImageQuery = 50,
StorageImageReadWithoutFormat = 55,
StorageImageWriteWithoutFormat = 56,
GroupNonUniform = 61,
GroupNonUniformVote = 62,
GroupNonUniformBallot = 64,
GroupNonUniformShuffle = 65,
RuntimeDescriptorArray = 5302,
}
internal enum SpirvStorageClass : uint
{
UniformConstant = 0,
Input = 1,
Uniform = 2,
Output = 3,
Workgroup = 4,
Private = 6,
Function = 7,
PushConstant = 9,
Image = 11,
StorageBuffer = 12,
}
internal enum SpirvExecutionModel : uint
{
Vertex = 0,
Fragment = 4,
GLCompute = 5,
}
internal enum SpirvExecutionMode : uint
{
OriginUpperLeft = 7,
DepthReplacing = 12,
LocalSize = 17,
}
internal enum SpirvDecoration : uint
{
Block = 2,
ArrayStride = 6,
BuiltIn = 11,
NoPerspective = 13,
Flat = 14,
Location = 30,
Binding = 33,
DescriptorSet = 34,
Offset = 35,
}
internal enum SpirvBuiltIn : uint
{
Position = 0,
VertexIndex = 42,
InstanceIndex = 43,
FragCoord = 15,
FrontFacing = 17,
WorkgroupId = 26,
LocalInvocationId = 27,
GlobalInvocationId = 28,
LocalInvocationIndex = 29,
SubgroupLocalInvocationId = 41,
}
internal enum SpirvImageDim : uint
{
Dim1D = 0,
Dim2D = 1,
Dim3D = 2,
Cube = 3,
Buffer = 5,
}
internal enum SpirvImageFormat : uint
{
Unknown = 0,
Rgba32f = 1,
Rgba16f = 2,
R32f = 3,
Rgba8 = 4,
Rgba8Snorm = 5,
Rg32f = 6,
Rg16f = 7,
R11fG11fB10f = 8,
R16f = 9,
Rgba16 = 10,
Rgb10A2 = 11,
Rg16 = 12,
Rg8 = 13,
R16 = 14,
R8 = 15,
Rgba16Snorm = 16,
Rg16Snorm = 17,
Rg8Snorm = 18,
R16Snorm = 19,
R8Snorm = 20,
Rgba32i = 21,
Rgba16i = 22,
Rgba8i = 23,
R32i = 24,
Rg32i = 25,
Rg16i = 26,
Rg8i = 27,
R16i = 28,
R8i = 29,
Rgba32ui = 30,
Rgba16ui = 31,
Rgba8ui = 32,
R32ui = 33,
Rgb10A2ui = 34,
Rg32ui = 35,
Rg16ui = 36,
Rg8ui = 37,
R16ui = 38,
R8ui = 39,
}
internal sealed class SpirvModuleBuilder
{
private const uint Magic = 0x07230203;
private const uint Version15 = 0x00010500;
private const uint Generator = 0x53504500; // "SPE"
private readonly List<uint> _capabilities = [];
private readonly List<uint> _extensions = [];
private readonly List<uint> _imports = [];
private readonly List<uint> _memoryModel = [];
private readonly List<uint> _entryPoints = [];
private readonly List<uint> _executionModes = [];
private readonly List<uint> _debug = [];
private readonly List<uint> _annotations = [];
private readonly List<uint> _typesConstantsGlobals = [];
private readonly List<uint> _functions = [];
private readonly Dictionary<(uint Width, bool Signed), uint> _integerTypes = [];
private readonly Dictionary<uint, uint> _floatTypes = [];
private readonly Dictionary<(uint Component, uint Count), uint> _vectorTypes = [];
private readonly Dictionary<
(
uint SampledType,
SpirvImageDim Dimension,
bool Depth,
bool Arrayed,
bool Multisampled,
uint Sampled,
SpirvImageFormat Format
),
uint> _imageTypes = [];
private readonly Dictionary<uint, uint> _sampledImageTypes = [];
private readonly Dictionary<(SpirvStorageClass Storage, uint Type), uint> _pointerTypes = [];
private readonly Dictionary<(uint Element, uint Count), uint> _arrayTypes = [];
private readonly Dictionary<uint, uint> _runtimeArrayTypes = [];
private readonly Dictionary<string, uint> _functionTypes = [];
private readonly Dictionary<(uint Type, ulong Value), uint> _constants = [];
private readonly HashSet<SpirvCapability> _declaredCapabilities = [];
private readonly Dictionary<string, uint> _extInstImports = [];
private uint _nextId = 1;
private uint? _voidType;
private uint? _boolType;
public uint AllocateId() => _nextId++;
public void AddCapability(SpirvCapability capability)
{
if (_declaredCapabilities.Add(capability))
{
Emit(_capabilities, SpirvOp.Capability, (uint)capability);
}
}
public void AddExtension(string extension) =>
EmitWithString(_extensions, SpirvOp.Extension, [], extension);
public uint ImportExtInst(string name)
{
if (_extInstImports.TryGetValue(name, out var existing))
{
return existing;
}
var id = AllocateId();
EmitWithString(_imports, SpirvOp.ExtInstImport, [id], name);
_extInstImports.Add(name, id);
return id;
}
public void SetLogicalGlsl450MemoryModel() =>
Emit(_memoryModel, SpirvOp.MemoryModel, 0, 1);
public void AddEntryPoint(
SpirvExecutionModel model,
uint function,
string name,
IReadOnlyList<uint> interfaces)
{
var prefix = new uint[2 + interfaces.Count];
prefix[0] = (uint)model;
prefix[1] = function;
for (var index = 0; index < interfaces.Count; index++)
{
prefix[index + 2] = interfaces[index];
}
EmitWithString(_entryPoints, SpirvOp.EntryPoint, prefix, name, stringBeforeTailCount: 2);
}
public void AddExecutionMode(uint function, SpirvExecutionMode mode, params uint[] operands)
{
var values = new uint[2 + operands.Length];
values[0] = function;
values[1] = (uint)mode;
operands.CopyTo(values, 2);
Emit(_executionModes, SpirvOp.ExecutionMode, values);
}
public void AddName(uint target, string name) =>
EmitWithString(_debug, SpirvOp.Name, [target], name);
public void AddDecoration(uint target, SpirvDecoration decoration, params uint[] operands)
{
var values = new uint[2 + operands.Length];
values[0] = target;
values[1] = (uint)decoration;
operands.CopyTo(values, 2);
Emit(_annotations, SpirvOp.Decorate, values);
}
public void AddMemberDecoration(
uint target,
uint member,
SpirvDecoration decoration,
params uint[] operands)
{
var values = new uint[3 + operands.Length];
values[0] = target;
values[1] = member;
values[2] = (uint)decoration;
operands.CopyTo(values, 3);
EmitRaw(_annotations, 72, values);
}
public uint TypeVoid()
{
if (_voidType is { } existing)
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeVoid, id);
_voidType = id;
return id;
}
public uint TypeBool()
{
if (_boolType is { } existing)
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeBool, id);
_boolType = id;
return id;
}
public uint TypeInt(uint width, bool signed)
{
var key = (width, signed);
if (_integerTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeInt, id, width, signed ? 1u : 0u);
_integerTypes.Add(key, id);
return id;
}
public uint TypeFloat(uint width)
{
if (_floatTypes.TryGetValue(width, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeFloat, id, width);
_floatTypes.Add(width, id);
return id;
}
public uint TypeVector(uint componentType, uint count)
{
var key = (componentType, count);
if (_vectorTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeVector, id, componentType, count);
_vectorTypes.Add(key, id);
return id;
}
public uint TypeImage(
uint sampledType,
SpirvImageDim dimension,
bool depth,
bool arrayed,
bool multisampled,
uint sampled,
SpirvImageFormat format)
{
var key = (
sampledType,
dimension,
depth,
arrayed,
multisampled,
sampled,
format);
if (_imageTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(
_typesConstantsGlobals,
SpirvOp.TypeImage,
id,
sampledType,
(uint)dimension,
depth ? 1u : 0u,
arrayed ? 1u : 0u,
multisampled ? 1u : 0u,
sampled,
(uint)format);
_imageTypes.Add(key, id);
return id;
}
public uint TypeSampledImage(uint imageType)
{
if (_sampledImageTypes.TryGetValue(imageType, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeSampledImage, id, imageType);
_sampledImageTypes.Add(imageType, id);
return id;
}
public uint TypeArray(uint elementType, uint count)
{
var key = (elementType, count);
if (_arrayTypes.TryGetValue(key, out var existing))
{
return existing;
}
var length = Constant(TypeInt(32, false), count);
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeArray, id, elementType, length);
_arrayTypes.Add(key, id);
return id;
}
public uint TypeRuntimeArray(uint elementType)
{
if (_runtimeArrayTypes.TryGetValue(elementType, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypeRuntimeArray, id, elementType);
_runtimeArrayTypes.Add(elementType, id);
return id;
}
public uint TypeStruct(params uint[] memberTypes)
{
var id = AllocateId();
var operands = new uint[memberTypes.Length + 1];
operands[0] = id;
memberTypes.CopyTo(operands, 1);
Emit(_typesConstantsGlobals, SpirvOp.TypeStruct, operands);
return id;
}
public uint TypePointer(SpirvStorageClass storageClass, uint type)
{
var key = (storageClass, type);
if (_pointerTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.TypePointer, id, (uint)storageClass, type);
_pointerTypes.Add(key, id);
return id;
}
public uint TypeFunction(uint returnType, params uint[] parameterTypes)
{
var key = returnType + ":" + string.Join(',', parameterTypes);
if (_functionTypes.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
var operands = new uint[parameterTypes.Length + 2];
operands[0] = id;
operands[1] = returnType;
parameterTypes.CopyTo(operands, 2);
Emit(_typesConstantsGlobals, SpirvOp.TypeFunction, operands);
_functionTypes.Add(key, id);
return id;
}
public uint ConstantBool(bool value)
{
var type = TypeBool();
var key = (type, value ? 1UL : 0UL);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(
_typesConstantsGlobals,
value ? SpirvOp.ConstantTrue : SpirvOp.ConstantFalse,
type,
id);
_constants.Add(key, id);
return id;
}
public uint Constant(uint type, uint value)
{
var key = (type, (ulong)value);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.Constant, type, id, value);
_constants.Add(key, id);
return id;
}
public uint Constant64(uint type, ulong value)
{
var key = (type, value);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(
_typesConstantsGlobals,
SpirvOp.Constant,
type,
id,
(uint)value,
(uint)(value >> 32));
_constants.Add(key, id);
return id;
}
public uint ConstantFloat(uint type, float value) =>
Constant(type, BitConverter.SingleToUInt32Bits(value));
public uint ConstantComposite(uint type, params uint[] constituents)
{
var id = AllocateId();
var operands = new uint[constituents.Length + 2];
operands[0] = type;
operands[1] = id;
constituents.CopyTo(operands, 2);
Emit(_typesConstantsGlobals, SpirvOp.ConstantComposite, operands);
return id;
}
public uint ConstantNull(uint type)
{
var key = (type, ulong.MaxValue);
if (_constants.TryGetValue(key, out var existing))
{
return existing;
}
var id = AllocateId();
Emit(_typesConstantsGlobals, SpirvOp.ConstantNull, type, id);
_constants.Add(key, id);
return id;
}
public uint AddGlobalVariable(
uint pointerType,
SpirvStorageClass storageClass,
uint? initializer = null)
{
var id = AllocateId();
if (initializer.HasValue)
{
Emit(
_typesConstantsGlobals,
SpirvOp.Variable,
pointerType,
id,
(uint)storageClass,
initializer.Value);
}
else
{
Emit(_typesConstantsGlobals, SpirvOp.Variable, pointerType, id, (uint)storageClass);
}
return id;
}
public uint BeginFunction(uint returnType, uint functionType)
{
var id = AllocateId();
Emit(_functions, SpirvOp.Function, returnType, id, 0, functionType);
return id;
}
public uint AddFunctionParameter(uint type) =>
EmitResult(_functions, SpirvOp.FunctionParameter, type);
public uint AddLabel(uint? id = null)
{
var result = id ?? AllocateId();
Emit(_functions, SpirvOp.Label, result);
return result;
}
public uint AddFunctionVariable(uint pointerType, uint? initializer = null)
{
var id = AllocateId();
if (initializer.HasValue)
{
Emit(
_functions,
SpirvOp.Variable,
pointerType,
id,
(uint)SpirvStorageClass.Function,
initializer.Value);
}
else
{
Emit(
_functions,
SpirvOp.Variable,
pointerType,
id,
(uint)SpirvStorageClass.Function);
}
return id;
}
public uint AddInstruction(SpirvOp opcode, uint resultType, params uint[] operands) =>
EmitResult(_functions, opcode, resultType, operands);
public void AddStatement(SpirvOp opcode, params uint[] operands) =>
Emit(_functions, opcode, operands);
public void EndFunction() => Emit(_functions, SpirvOp.FunctionEnd);
public byte[] Build()
{
if (_memoryModel.Count == 0)
{
SetLogicalGlsl450MemoryModel();
}
var wordCount =
5 +
_capabilities.Count +
_extensions.Count +
_imports.Count +
_memoryModel.Count +
_entryPoints.Count +
_executionModes.Count +
_debug.Count +
_annotations.Count +
_typesConstantsGlobals.Count +
_functions.Count;
var words = new uint[wordCount];
var offset = 0;
WriteWord(Magic);
WriteWord(Version15);
WriteWord(Generator);
WriteWord(_nextId);
WriteWord(0);
WriteSection(_capabilities);
WriteSection(_extensions);
WriteSection(_imports);
WriteSection(_memoryModel);
WriteSection(_entryPoints);
WriteSection(_executionModes);
WriteSection(_debug);
WriteSection(_annotations);
WriteSection(_typesConstantsGlobals);
WriteSection(_functions);
var bytes = new byte[wordCount * sizeof(uint)];
Buffer.BlockCopy(words, 0, bytes, 0, bytes.Length);
return bytes;
void WriteWord(uint value)
{
words[offset++] = value;
}
void WriteSection(List<uint> section)
{
foreach (var value in section)
{
WriteWord(value);
}
}
}
private uint EmitResult(
List<uint> section,
SpirvOp opcode,
uint resultType,
params uint[] operands)
{
var result = AllocateId();
var values = new uint[operands.Length + 2];
values[0] = resultType;
values[1] = result;
operands.CopyTo(values, 2);
Emit(section, opcode, values);
return result;
}
private static void Emit(List<uint> section, SpirvOp opcode, params uint[] operands) =>
EmitRaw(section, (ushort)opcode, operands);
private static void EmitRaw(List<uint> section, ushort opcode, params uint[] operands)
{
section.Add(((uint)(operands.Length + 1) << 16) | opcode);
section.AddRange(operands);
}
private static void EmitWithString(
List<uint> section,
SpirvOp opcode,
IReadOnlyList<uint> prefix,
string value,
int stringBeforeTailCount = -1)
{
var encoded = EncodeString(value);
if (stringBeforeTailCount < 0)
{
var operands = new uint[prefix.Count + encoded.Length];
for (var index = 0; index < prefix.Count; index++)
{
operands[index] = prefix[index];
}
encoded.CopyTo(operands, prefix.Count);
Emit(section, opcode, operands);
return;
}
var result = new uint[prefix.Count + encoded.Length];
for (var index = 0; index < stringBeforeTailCount; index++)
{
result[index] = prefix[index];
}
encoded.CopyTo(result, stringBeforeTailCount);
for (var index = stringBeforeTailCount; index < prefix.Count; index++)
{
result[index + encoded.Length] = prefix[index];
}
Emit(section, opcode, result);
}
private static uint[] EncodeString(string value)
{
var byteCount = Encoding.UTF8.GetByteCount(value) + 1;
var words = new uint[(byteCount + 3) / 4];
Encoding.UTF8.GetBytes(value, System.Runtime.InteropServices.MemoryMarshal.AsBytes(words.AsSpan()));
return words;
}
}
+26 -88
View File
@@ -24,7 +24,6 @@ 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 =
@@ -38,23 +37,6 @@ 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",
@@ -267,7 +249,7 @@ public static class AmprExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
{
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
@@ -453,7 +435,7 @@ public static class AmprExports
var offset = 0UL;
while (offset < writeOffset)
{
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -652,43 +634,24 @@ public static class AmprExports
try
{
if (!TryGetCachedHostFile(hostPath, out var cachedFile, out var openResult))
{
return openResult;
}
long fileLength;
lock (cachedFile.Gate)
{
fileLength = cachedFile.Stream.Length;
}
if (fileOffset >= (ulong)fileLength)
using var stream = new FileStream(
hostPath,
FileMode.Open,
FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete,
ChunkSize,
FileOptions.SequentialScan);
if (fileOffset >= (ulong)stream.Length)
{
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
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);
int read;
lock (cachedFile.Gate)
{
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
read = cachedFile.Stream.Read(buffer, 0, request);
}
var read = stream.Read(buffer, 0, request);
if (read <= 0)
{
break;
@@ -718,44 +681,6 @@ 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,
@@ -883,6 +808,19 @@ 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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
int value;
@@ -83,18 +83,18 @@ public static class AppContentExports
}
else if (!TryReadUserDefinedParam(paramId, out value))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceAppContent($"app_param_get_int id={paramId} value={value}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -168,6 +168,12 @@ 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))
+41 -27
View File
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.Audio;
@@ -36,7 +37,7 @@ public static class AudioOut2Exports
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
@@ -47,8 +48,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
return ctx.Memory.TryWrite(paramAddress, param)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -62,7 +63,7 @@ public static class AudioOut2Exports
var memoryInfoAddress = ctx[CpuRegister.Rsi];
if (paramAddress == 0 || memoryInfoAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> memoryInfo = stackalloc byte[0x20];
@@ -73,8 +74,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -90,13 +91,13 @@ public static class AudioOut2Exports
var outContextAddress = ctx[CpuRegister.Rcx];
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
return ctx.TryWriteUInt64(outContextAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return TryWriteUInt64(ctx, outContextAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -104,7 +105,7 @@ public static class AudioOut2Exports
ExportName = "sceAudioOut2ContextDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
public static int AudioOut2ContextDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "JK2wamZPzwM",
@@ -119,14 +120,14 @@ public static class AudioOut2Exports
var contextAddress = ctx[CpuRegister.Rcx];
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
return ctx.TryWriteUInt64(outPortAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return TryWriteUInt64(ctx, outPortAddress, handle)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -140,7 +141,7 @@ public static class AudioOut2Exports
var stateAddress = ctx[CpuRegister.Rsi];
if (handle == 0 || stateAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var type = (int)((handle >> 16) & 0xFF);
@@ -153,8 +154,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
return ctx.Memory.TryWrite(stateAddress, state)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -167,7 +168,7 @@ public static class AudioOut2Exports
var infoAddress = ctx[CpuRegister.Rdi];
if (infoAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> info = stackalloc byte[0x40];
@@ -177,8 +178,8 @@ public static class AudioOut2Exports
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
return ctx.Memory.TryWrite(infoAddress, info)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -186,14 +187,14 @@ public static class AudioOut2Exports
ExportName = "sceAudioOut2PortDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "IaZXJ9M79uo",
ExportName = "sceAudioOut2UserDestroy",
Target = Generation.Gen5,
LibraryName = "libSceAudioOut2")]
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
[SysAbiExport(
Nid = "xywYcRB7nbQ",
@@ -206,12 +207,25 @@ public static class AudioOut2Exports
var outUserAddress = ctx[CpuRegister.Rsi];
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
return ctx.TryWriteUInt64(outUserAddress, handle)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
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;
}
}
-234
View File
@@ -1,234 +0,0 @@
// 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;
}
}
-302
View File
@@ -1,302 +0,0 @@
// 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);
}
@@ -1,68 +0,0 @@
// 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);
}
}
}
@@ -1,27 +0,0 @@
// 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;
}
}
+181 -110
View File
@@ -104,17 +104,17 @@ public static class FiberExports
var optParam = ctx[CpuRegister.Rdi];
if (optParam == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if ((optParam & 7) != 0)
{
return ctx.SetReturn(FiberErrorAlignment);
return SetReturn(ctx, FiberErrorAlignment);
}
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
@@ -127,24 +127,24 @@ public static class FiberExports
var fiber = ctx[CpuRegister.Rdi];
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (state != FiberStateIdle)
{
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
_continuations.TryRemove(fiber, out _);
_returnTargets.TryRemove(fiber, out _);
_stackRanges.TryRemove(fiber, out _);
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
return ctx.SetReturn(0);
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -229,20 +229,20 @@ public static class FiberExports
var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0)
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
var returnArgument = ctx[CpuRegister.Rdi];
var argOnRunAddress = ctx[CpuRegister.Rsi];
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
if (argOnRunAddress != 0 && !TryWriteUInt64(ctx, argOnRunAddress, 0))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
GuestCpuContinuation transferTarget;
@@ -256,7 +256,7 @@ public static class FiberExports
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
{
_continuations.TryRemove(fiberAddress, out _);
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
previousFiber = returnTarget.PreviousFiber;
@@ -264,10 +264,10 @@ public static class FiberExports
{
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun))
{
_continuations.TryRemove(fiberAddress, out _);
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, 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 ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
}
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, 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 ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -309,18 +309,18 @@ public static class FiberExports
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
var fiberAddress = ResolveCurrentFiberAddress(ctx);
if (fiberAddress == 0)
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
return ctx.TryWriteUInt64(outAddress, fiberAddress)
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
return TryWriteUInt64(ctx, outAddress, fiberAddress)
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
@@ -334,35 +334,35 @@ public static class FiberExports
var info = ctx[CpuRegister.Rsi];
if (info == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
if (!TryReadUInt64(ctx, info, out var size) || size != FiberInfoSize)
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (!TryReadFiberFields(ctx, fiber, out var fields))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
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) ||
!TryWriteName(ctx, info + 40, fields.Name) ||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -376,22 +376,22 @@ public static class FiberExports
var nameAddress = ctx[CpuRegister.Rsi];
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (nameAddress == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
return TryWriteName(ctx, fiber + FiberNameOffset, name)
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
[SysAbiExport(
@@ -403,12 +403,12 @@ public static class FiberExports
{
if (ctx[CpuRegister.Rdi] != 0)
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorState);
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorState);
}
[SysAbiExport(
@@ -419,8 +419,8 @@ public static class FiberExports
public static int FiberStopContextSizeCheck(CpuContext ctx)
{
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorState);
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorState);
}
[SysAbiExport(
@@ -433,17 +433,17 @@ public static class FiberExports
var outAddress = ctx[CpuRegister.Rdi];
if (outAddress == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if (ResolveCurrentFiberAddress(ctx) == 0)
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
? ctx.SetReturn(0)
: ctx.SetReturn(FiberErrorInvalid);
return TryWriteUInt64(ctx, outAddress, ctx[CpuRegister.Rbp])
? SetReturn(ctx, 0)
: SetReturn(ctx, FiberErrorInvalid);
}
private static int FiberInitializeCore(
@@ -459,37 +459,37 @@ public static class FiberExports
{
if (fiber == 0 || nameAddress == 0 || entry == 0)
{
return ctx.SetReturn(FiberErrorNull);
return SetReturn(ctx, FiberErrorNull);
}
if ((fiber & 7) != 0 ||
(contextAddress & 15) != 0 ||
(optParam & 7) != 0)
{
return ctx.SetReturn(FiberErrorAlignment);
return SetReturn(ctx, FiberErrorAlignment);
}
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
{
return ctx.SetReturn(FiberErrorRange);
return SetReturn(ctx, FiberErrorRange);
}
if ((contextSize & 15) != 0 ||
(contextAddress == 0 && contextSize != 0) ||
(contextAddress != 0 && contextSize == 0))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (optParam != 0 &&
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (Volatile.Read(ref _contextSizeCheck) != 0)
@@ -497,27 +497,27 @@ public static class FiberExports
flags |= FiberFlagContextSizeCheck;
}
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
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) ||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
!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))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (contextAddress != 0)
{
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
if (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, 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 ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
private static int FiberRunCore(
@@ -547,12 +547,12 @@ public static class FiberExports
{
if (!TryValidateFiber(ctx, fiber, out var error))
{
return ctx.SetReturn(error);
return SetReturn(ctx, error);
}
if (!TryReadFiberFields(ctx, fiber, out var fields))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, 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 ctx.SetReturn(attachResult);
return SetReturn(ctx, attachResult);
}
}
@@ -568,16 +568,16 @@ public static class FiberExports
if ((isSwitch && previousFiber == 0) ||
(!isSwitch && previousFiber != 0))
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
if (previousFiber == fiber)
{
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
{
return ctx.SetReturn(FiberErrorPermission);
return SetReturn(ctx, FiberErrorPermission);
}
GuestCpuContinuation transferTarget;
@@ -586,12 +586,12 @@ public static class FiberExports
{
if (!TryReadFiberFields(ctx, fiber, out fields))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (fields.State != FiberStateIdle)
{
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
FiberContinuation targetContinuation;
@@ -602,12 +602,12 @@ public static class FiberExports
}
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
{
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, FiberErrorInvalid);
}
var callerContinuation = new FiberContinuation(
@@ -616,11 +616,11 @@ public static class FiberExports
if (previousFiber != 0)
{
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
previousState != FiberStateRun ||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
{
return ctx.SetReturn(FiberErrorState);
return SetReturn(ctx, FiberErrorState);
}
_continuations[previousFiber] = callerContinuation;
@@ -631,15 +631,15 @@ public static class FiberExports
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
}
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
{
if (previousFiber != 0)
{
_continuations.TryRemove(previousFiber, out _);
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
}
_returnTargets.TryRemove(fiber, out _);
return ctx.SetReturn(FiberErrorInvalid);
return SetReturn(ctx, 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 ctx.SetReturn(0);
return SetReturn(ctx, 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 (!ctx.TryWriteUInt64(entryRsp, 0))
if (!TryWriteUInt64(ctx, entryRsp, 0))
{
return false;
}
@@ -708,7 +708,7 @@ public static class FiberExports
FiberContinuation continuation,
ulong argument) =>
continuation.ArgOnRunAddress == 0 ||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
private static GuestCpuContinuation CaptureContinuation(
CpuContext ctx,
@@ -779,11 +779,11 @@ public static class FiberExports
return FiberErrorInvalid;
}
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
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))
{
return FiberErrorInvalid;
}
@@ -839,8 +839,8 @@ public static class FiberExports
return false;
}
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
!TryReadUInt32(ctx, 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 (!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) ||
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) ||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
{
return false;
@@ -898,6 +898,46 @@ 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];
@@ -925,6 +965,37 @@ 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,6 +3,7 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Ampr;
using System.Buffers.Binary;
using System.Collections.Concurrent;
using System.Threading;
@@ -49,7 +50,7 @@ public static class KernelAprCompatExports
return completionResult;
}
if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId))
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -140,7 +141,7 @@ public static class KernelAprCompatExports
return completionResult;
}
if (!ctx.TryWriteUInt32(outSubmissionId, submissionId))
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -177,6 +178,13 @@ 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,6 +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;
@@ -51,17 +52,17 @@ public static class KernelEventFlagCompatExports
optionAddress != 0 ||
!IsValidAttributes(attributes))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
@@ -75,11 +76,11 @@ public static class KernelEventFlagCompatExports
if (!ctx.TryWriteUInt64(outAddress, handle))
{
_eventFlags.TryRemove(handle, out _);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -92,7 +93,7 @@ public static class KernelEventFlagCompatExports
var handle = ctx[CpuRegister.Rdi];
if (!_eventFlags.TryRemove(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
@@ -101,7 +102,7 @@ public static class KernelEventFlagCompatExports
}
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -116,7 +117,7 @@ public static class KernelEventFlagCompatExports
var returnRip = GetCurrentReturnRip();
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
@@ -127,7 +128,7 @@ public static class KernelEventFlagCompatExports
}
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -141,7 +142,7 @@ public static class KernelEventFlagCompatExports
var pattern = ctx[CpuRegister.Rsi];
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
@@ -150,7 +151,7 @@ public static class KernelEventFlagCompatExports
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -167,29 +168,29 @@ public static class KernelEventFlagCompatExports
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (pattern == 0 || !IsValidWaitMode(waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (state.Gate)
{
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (!IsSatisfied(state.Bits, pattern, waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
ApplyClearMode(state, pattern, waitMode);
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
@@ -209,18 +210,18 @@ public static class KernelEventFlagCompatExports
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (pattern == 0 || !IsValidWaitMode(waitMode))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
uint timeoutUsec = 0;
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Monitor.Enter(state.Gate);
@@ -228,15 +229,15 @@ public static class KernelEventFlagCompatExports
{
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
{
return ctx.SetReturn(immediateWaitResult);
return SetReturn(ctx, immediateWaitResult);
}
if (timeoutAddress != 0)
{
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
@@ -271,7 +272,7 @@ public static class KernelEventFlagCompatExports
var scheduler = GuestThreadExecution.Scheduler;
if (scheduler is null)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
state.WaitingThreads++;
@@ -296,7 +297,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 ctx.SetReturn(pumpedWaitResult);
return SetReturn(ctx, pumpedWaitResult);
}
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
@@ -313,7 +314,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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
finally
{
@@ -333,15 +334,15 @@ public static class KernelEventFlagCompatExports
var waiterCountAddress = ctx[CpuRegister.Rdx];
if (!_eventFlags.TryGetValue(handle, out var state))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
lock (state.Gate)
{
if (waiterCountAddress != 0 &&
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
state.Bits = setPattern;
@@ -350,7 +351,7 @@ public static class KernelEventFlagCompatExports
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
}
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static bool IsValidAttributes(uint attributes)
@@ -452,6 +453,84 @@ 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))
@@ -535,7 +614,7 @@ public static class KernelEventFlagCompatExports
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadByte(address, out var value))
if (TryReadByte(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X2}");
}
@@ -543,7 +622,7 @@ public static class KernelEventFlagCompatExports
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadUInt32(address, out var value))
if (TryReadUInt32(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X8}");
}
@@ -551,7 +630,7 @@ public static class KernelEventFlagCompatExports
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
{
if (ctx.TryReadUInt64(address, out var value))
if (TryReadUInt64(ctx, address, out var value))
{
builder.Append($" {name}=0x{value:X16}");
}
@@ -276,24 +276,8 @@ 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 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -316,41 +300,6 @@ 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;
}
@@ -381,7 +330,6 @@ public static class KernelEventQueueCompatExports
queue.AddLast(queuedEvent);
queued = true;
Monitor.PulseAll(_eventQueueGate);
}
if (queued)
@@ -541,14 +489,12 @@ public static class KernelEventQueueCompatExports
ulong outCountAddress)
{
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
return deliveredCount > 0
? (int)OrbisGen2Result.ORBIS_GEN2_OK
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool HasPendingEvents(ulong handle)
@@ -654,8 +600,16 @@ public static class KernelEventQueueCompatExports
return;
}
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
var returnRip = 0UL;
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
Console.Error.WriteLine(
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
}
@@ -1,65 +0,0 @@
// 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;
}
}
+1 -7
View File
@@ -8,6 +8,7 @@ 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();
@@ -364,13 +365,6 @@ 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",
@@ -5,9 +5,11 @@ 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;
@@ -101,12 +103,10 @@ public static class KernelMemoryCompatExports
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;
@@ -231,25 +231,6 @@ 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",
@@ -1341,7 +1322,6 @@ public static class KernelMemoryCompatExports
if (IsMutatingOpen(flags))
{
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
InvalidateAprFileSizeCache(hostPath);
}
LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}");
@@ -1550,7 +1530,6 @@ public static class KernelMemoryCompatExports
File.Delete(hostPath);
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
InvalidateAprFileSizeCache(hostPath);
AddNegativeStatCacheForGuestPath(guestPath);
LogOpenTrace($"unlink path='{guestPath}' host='{hostPath}'");
ctx[CpuRegister.Rax] = 0;
@@ -2456,36 +2435,6 @@ 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",
@@ -2529,17 +2478,6 @@ 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
{
@@ -2550,7 +2488,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 && !fixedMapping)
if (!reserved)
{
if (mappedAddress == 0)
{
@@ -2608,11 +2546,8 @@ public static class KernelMemoryCompatExports
var length = ctx[CpuRegister.Rsi];
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
var flags = ctx[CpuRegister.Rcx];
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}");
}
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;
@@ -2634,25 +2569,16 @@ 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 = AllocateMappedGuestAddress(ctx, length, 0x1000UL);
mappedAddress = requestedAddress != 0 && fixedMapping
? requestedAddress
: AllocateMappedGuestAddress(ctx, length, 0x1000UL);
}
if (ShouldTraceDirectMemory())
{
Console.Error.WriteLine(
$"[LOADER][TRACE] map_flexible reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16}");
}
Console.Error.WriteLine(
$"[LOADER][TRACE] map_flexible reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16}");
if (mappedAddress == 0)
{
@@ -2688,16 +2614,6 @@ 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",
@@ -2751,44 +2667,6 @@ 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",
@@ -2846,16 +2724,11 @@ public static class KernelMemoryCompatExports
BinaryPrimitives.WriteInt32LittleEndian(payload[28..32], memoryType);
payload[32] = unchecked((byte)stateFlags);
string name;
lock (_memoryGate)
{
name = _mappedRegionNames.GetValueOrDefault(region.Address)
?? (region.IsDirect
? "direct"
: region.IsFlexible
? "flexible"
: string.Empty);
}
var name = region.IsDirect
? "direct"
: region.IsFlexible
? "flexible"
: string.Empty;
if (!string.IsNullOrEmpty(name))
{
var nameBytes = Encoding.ASCII.GetBytes(name);
@@ -2903,7 +2776,7 @@ public static class KernelMemoryCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
if (protectionOut != 0 && !ctx.TryWriteInt32(protectionOut, region.Protection))
if (protectionOut != 0 && !TryWriteInt32(ctx, protectionOut, region.Protection))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -2942,7 +2815,7 @@ public static class KernelMemoryCompatExports
if (!ctx.TryWriteUInt64(infoAddress, block.Start) ||
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), block.Start + block.Length) ||
!ctx.TryWriteInt32(infoAddress + (sizeof(ulong) * 2), block.MemoryType))
!TryWriteInt32(ctx, infoAddress + (sizeof(ulong) * 2), block.MemoryType))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -3800,7 +3673,7 @@ public static class KernelMemoryCompatExports
var addr = argumentSource.NextGpArg();
if (addr != 0)
{
_ = ctx.TryWriteInt32(addr, sb.Length);
_ = TryWriteInt32(ctx, addr, sb.Length);
}
}
break;
@@ -3997,48 +3870,118 @@ public static class KernelMemoryCompatExports
ulong alignment,
out ulong mappedAddress)
{
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)
mappedAddress = 0;
if (length == 0)
{
return false;
}
Span<byte> probe = stackalloc byte[1];
return ctx.Memory.TryRead(address, probe) &&
ctx.Memory.TryRead(address + length - 1, probe);
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;
}
}
private static bool IsMappedGuestRangeAvailable(
@@ -4191,20 +4134,6 @@ 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))
{
@@ -4525,7 +4454,7 @@ public static class KernelMemoryCompatExports
continue;
}
var one = chunk.AsSpan(0, 1);
Span<byte> one = stackalloc byte[1];
if (!TryReadCompat(ctx, current, one))
{
return false;
@@ -4964,7 +4893,7 @@ public static class KernelMemoryCompatExports
processedCount++;
}
if (processedOutAddress != 0 && !ctx.TryWriteInt32(processedOutAddress, processedCount))
if (processedOutAddress != 0 && !TryWriteInt32(ctx, processedOutAddress, processedCount))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -5459,37 +5388,6 @@ public static class KernelMemoryCompatExports
}
}
internal static bool TryReadTrackedLibcHeap(
ulong address,
Span<byte> destination)
{
if (destination.IsEmpty)
{
return true;
}
var length = (ulong)destination.Length;
lock (_libcAllocGate)
{
foreach (var (allocationAddress, allocation) in _libcAllocations)
{
var allocationSize = (ulong)allocation.Size;
var offset = address >= allocationAddress
? address - allocationAddress
: ulong.MaxValue;
if (offset > allocationSize ||
length > allocationSize - offset)
{
continue;
}
return TryReadHostMemory(address, destination);
}
}
return false;
}
private static bool TryAllocateLibcHeap(ulong requestedSize, nuint alignment, bool zeroFill, out ulong address)
{
address = 0;
@@ -5801,6 +5699,13 @@ 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)
@@ -5860,40 +5765,22 @@ public static class KernelMemoryCompatExports
private static bool TryGetAprFileSize(string hostPath, out ulong size)
{
size = 0;
string cachePath;
try
{
cachePath = Path.GetFullPath(hostPath);
}
catch
{
cachePath = hostPath;
}
if (_aprFileSizeCache.TryGetValue(cachePath, out size))
{
return true;
}
try
{
var fileInfo = new FileInfo(cachePath);
var fileInfo = new FileInfo(hostPath);
if (fileInfo.Exists)
{
var length = fileInfo.Length;
size = length < 0 ? 0UL : unchecked((ulong)length);
_aprFileSizeCache.TryAdd(cachePath, size);
return true;
}
if (!new DirectoryInfo(cachePath).Exists)
if (!new DirectoryInfo(hostPath).Exists)
{
return false;
}
size = 65536;
_aprFileSizeCache.TryAdd(cachePath, size);
return true;
}
catch
@@ -6192,20 +6079,6 @@ 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,6 +3,8 @@
using System.Collections.Concurrent;
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
using System.Diagnostics.CodeAnalysis;
namespace SharpEmu.Libs.Kernel;
@@ -93,13 +95,6 @@ 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",
@@ -327,12 +322,6 @@ 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",
@@ -387,42 +376,42 @@ public static class KernelPthreadCompatExports
var initRoutine = ctx[CpuRegister.Rsi];
if (onceAddress == 0 || initRoutine == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryReadInt32(onceAddress, out var onceValue))
if (!TryReadInt32(ctx, onceAddress, out var onceValue))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (onceValue == PthreadOnceDone)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
var gate = GetPthreadOnceGate(onceAddress);
var shouldCall = false;
lock (gate)
{
if (!ctx.TryReadInt32(onceAddress, out onceValue))
if (!TryReadInt32(ctx, onceAddress, out onceValue))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
while (onceValue == PthreadOnceInProgress)
{
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
if (!ctx.TryReadInt32(onceAddress, out onceValue))
if (!TryReadInt32(ctx, onceAddress, out onceValue))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
if (onceValue != PthreadOnceDone)
{
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceInProgress))
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceInProgress))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
shouldCall = true;
@@ -438,21 +427,21 @@ public static class KernelPthreadCompatExports
{
lock (gate)
{
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
Monitor.PulseAll(gate);
}
TracePthreadOnce(onceAddress, initRoutine, "failed", error);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
lock (gate)
{
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceDone))
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceDone))
{
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
Monitor.PulseAll(gate);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Monitor.PulseAll(gate);
@@ -460,7 +449,7 @@ public static class KernelPthreadCompatExports
}
TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
@@ -1022,12 +1011,19 @@ public static class KernelPthreadCompatExports
}
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
ctx.TryWriteUInt32(address + 0x20, unchecked((uint)state.Type)) &&
ctx.TryWriteUInt32(address + 0x3C, unchecked((uint)state.Protocol));
TryWriteUInt32(ctx, address + 0x20, unchecked((uint)state.Type)) &&
TryWriteUInt32(ctx, address + 0x3C, unchecked((uint)state.Protocol));
private static bool WriteMutexAttrObject(CpuContext ctx, ulong address, PthreadMutexAttrState state) =>
ctx.TryWriteUInt32(address, unchecked((uint)state.Type)) &&
ctx.TryWriteUInt32(address + 4, unchecked((uint)state.Protocol));
TryWriteUInt32(ctx, address, unchecked((uint)state.Type)) &&
TryWriteUInt32(ctx, address + 4, unchecked((uint)state.Protocol));
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> bytes = stackalloc byte[sizeof(uint)];
BitConverter.TryWriteBytes(bytes, value);
return ctx.Memory.TryWrite(address, bytes);
}
private static int PthreadCondInitCore(CpuContext ctx, ulong condAddress)
{
@@ -1319,6 +1315,32 @@ 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,8 +2,10 @@
// 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;
@@ -20,7 +22,6 @@ 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();
@@ -30,7 +31,6 @@ 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 (!ctx.TryWriteInt32(outPriorityAddress, priority))
if (!TryWriteInt32(ctx, 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 (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -494,7 +494,7 @@ public static class KernelPthreadExtendedCompatExports
state = GetOrCreateAttrStateLocked(attrAddress);
}
if (!ctx.TryWriteInt32(outStateAddress, state.DetachState))
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -717,35 +717,6 @@ 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",
@@ -760,7 +731,7 @@ public static class KernelPthreadExtendedCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -839,37 +810,6 @@ 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",
@@ -1047,47 +987,6 @@ 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",
@@ -1112,12 +1011,12 @@ public static class KernelPthreadExtendedCompatExports
}
}
if (!ctx.TryWriteInt32(outKeyAddress, key))
if (!TryWriteInt32(ctx, outKeyAddress, key))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
ctx[CpuRegister.Rax] = unchecked((uint)key);
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
@@ -1449,4 +1348,24 @@ 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,11 +4,15 @@
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;
@@ -83,11 +87,11 @@ public static class KernelRuntimeCompatExports
if (micros < 1000)
{
// 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)
// Guest worker pools use usleep(1) as a polling backoff. Periodically
// relinquish a full host time slice so spin workers cannot starve producers.
if ((++_shortUsleepCount & 31) == 0)
{
Thread.Sleep(0);
Thread.Sleep(1);
}
else
{
@@ -256,8 +260,8 @@ public static class KernelRuntimeCompatExports
}
if (timezoneAddress != 0 &&
(!ctx.TryWriteInt32(timezoneAddress, 0) ||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), 0)))
(!TryWriteInt32(ctx, timezoneAddress, 0) ||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), 0)))
{
return -1;
}
@@ -614,7 +618,7 @@ public static class KernelRuntimeCompatExports
internal static bool TrySetErrno(CpuContext ctx, int value)
{
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
return address != 0 && ctx.TryWriteInt32(address, value);
return address != 0 && TryWriteInt32(ctx, address, value);
}
[SysAbiExport(
@@ -735,11 +739,8 @@ public static class KernelRuntimeCompatExports
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
}
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}");
}
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))
{
@@ -751,7 +752,6 @@ 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 (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -854,7 +854,7 @@ public static class KernelRuntimeCompatExports
}
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
if (!TryWriteInt32(ctx, 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 && !ctx.TryWriteInt32(resultAddress, 0))
if (resultAddress != 0 && !TryWriteInt32(ctx, 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 && !ctx.TryWriteInt32(resultAddress, 0))
if (resultAddress != 0 && !TryWriteInt32(ctx, 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 (!ctx.TryWriteInt32(timezoneAddress, minutesWest) ||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), dstSeconds / 60))
if (!TryWriteInt32(ctx, timezoneAddress, minutesWest) ||
!TryWriteInt32(ctx, 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 && !ctx.TryWriteInt32(dstSecondsAddress, dstSeconds))
if (dstSecondsAddress != 0 && !TryWriteInt32(ctx, dstSecondsAddress, dstSeconds))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -1301,7 +1301,7 @@ public static class KernelRuntimeCompatExports
_loadedSysmodules.Add(moduleId);
}
if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
if (resultAddress != 0 && !TryWriteInt32(ctx, 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 (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, module.Handle))
if (!TryWriteInt32(ctx, 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 (!ctx.TryWriteInt32(handlesAddress + (ulong)(i * sizeof(int)), handles[i]))
if (!TryWriteInt32(ctx, handlesAddress + (ulong)(i * sizeof(int)), handles[i]))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -1501,6 +1501,13 @@ 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
@@ -1725,16 +1732,117 @@ public static class KernelRuntimeCompatExports
bool allowSearch,
out ulong mappedAddress)
{
return KernelVirtualRangeAllocator.TryReserve(
ctx,
desiredAddress,
length,
executable: false,
alignment,
allowSearch,
allowAllocateAtAlternative: allowSearch,
"reserve_virtual_range",
out 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;
}
}
private static ulong AlignUp(ulong value, ulong alignment)
@@ -1747,9 +1855,4 @@ 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,7 +1,9 @@
// 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;
@@ -44,12 +46,12 @@ public static class KernelSemaphoreCompatExports
initialCount > maxCount ||
optionAddress != 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
@@ -66,14 +68,14 @@ public static class KernelSemaphoreCompatExports
Count = initialCount,
};
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
{
_semaphores.TryRemove(handle, out _);
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -89,12 +91,12 @@ public static class KernelSemaphoreCompatExports
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (needCount < 1 || needCount > semaphore.MaxCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
@@ -103,30 +105,30 @@ public static class KernelSemaphoreCompatExports
{
semaphore.Count -= needCount;
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
if (timeoutAddress != 0)
{
if (!ctx.TryReadUInt32(timeoutAddress, out _))
if (!TryReadUInt32(ctx, timeoutAddress, out _))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
}
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
{
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
}
semaphore.WaitingThreads++;
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
@@ -142,12 +144,12 @@ public static class KernelSemaphoreCompatExports
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (needCount < 1 || needCount > semaphore.MaxCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
@@ -155,12 +157,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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
}
semaphore.Count -= needCount;
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
@@ -176,24 +178,24 @@ public static class KernelSemaphoreCompatExports
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (signalCount <= 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (semaphore.Count > semaphore.MaxCount - signalCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
semaphore.Count += signalCount;
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
@@ -210,25 +212,25 @@ public static class KernelSemaphoreCompatExports
if (!_semaphores.TryGetValue(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (setCount > semaphore.MaxCount)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
lock (semaphore.Gate)
{
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
semaphore.WaitingThreads = 0;
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
}
@@ -242,11 +244,68 @@ public static class KernelSemaphoreCompatExports
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
if (!_semaphores.TryRemove(handle, out var semaphore))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
var value = (int)result;
ctx[CpuRegister.Rax] = unchecked((ulong)value);
return value;
}
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
if (!ctx.Memory.TryRead(address, buffer))
{
value = 0;
return false;
}
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
return true;
}
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
{
Span<byte> buffer = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
return ctx.Memory.TryWrite(address, buffer);
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
{
value = string.Empty;
if (address == 0 || maxLength <= 0)
{
return false;
}
var bytes = new byte[Math.Min(maxLength, 4096)];
for (var i = 0; i < bytes.Length; i++)
{
Span<byte> current = stackalloc byte[1];
if (!ctx.Memory.TryRead(address + (ulong)i, current))
{
return false;
}
if (current[0] == 0)
{
value = Encoding.UTF8.GetString(bytes, 0, i);
return true;
}
bytes[i] = current[0];
}
value = Encoding.UTF8.GetString(bytes);
return true;
}
private static void TraceSemaphore(string message)
@@ -1,159 +0,0 @@
// 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);
}
+11 -3
View File
@@ -1,7 +1,9 @@
// 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;
@@ -30,7 +32,7 @@ public static class Http2Exports
if (poolSize == 0 || maxRequests <= 0)
{
return ctx.SetReturn(Http2ErrorInvalidArgument);
return SetReturn(ctx, Http2ErrorInvalidArgument);
}
var id = Interlocked.Increment(ref _nextContextId);
@@ -51,11 +53,17 @@ public static class Http2Exports
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_contexts.TryRemove(id, out _))
{
return ctx.SetReturn(Http2ErrorInvalidId);
return SetReturn(ctx, Http2ErrorInvalidId);
}
TraceHttp2("term", id, 0, 0, 0, 0);
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceHttp2(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
+13 -6
View File
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using System.Collections.Concurrent;
using System.Threading;
namespace SharpEmu.Libs.Network;
@@ -32,7 +33,7 @@ public static class HttpExports
var poolSize = ctx[CpuRegister.Rdx];
if (poolSize == 0)
{
return ctx.SetReturn(HttpErrorInvalidValue);
return SetReturn(ctx, HttpErrorInvalidValue);
}
var id = Interlocked.Increment(ref _nextContextId);
@@ -52,7 +53,7 @@ public static class HttpExports
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Contexts.ContainsKey(contextId))
{
return ctx.SetReturn(HttpErrorInvalidId);
return SetReturn(ctx, HttpErrorInvalidId);
}
var userAgentAddress = ctx[CpuRegister.Rsi];
@@ -74,8 +75,8 @@ public static class HttpExports
{
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
return Templates.TryRemove(templateId, out _)
? ctx.SetReturn(0)
: ctx.SetReturn(HttpErrorInvalidId);
? SetReturn(ctx, 0)
: SetReturn(ctx, HttpErrorInvalidId);
}
[SysAbiExport(
@@ -88,7 +89,7 @@ public static class HttpExports
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
if (!Contexts.TryRemove(contextId, out _))
{
return ctx.SetReturn(HttpErrorInvalidId);
return SetReturn(ctx, HttpErrorInvalidId);
}
foreach (var pair in Templates)
@@ -99,7 +100,13 @@ public static class HttpExports
}
}
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
+29 -23
View File
@@ -57,27 +57,27 @@ public static class NetCtlExports
var natInfoAddress = ctx[CpuRegister.Rdi];
if (natInfoAddress == 0)
{
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
Span<byte> natInfo = stackalloc byte[NatInfoSize];
if (!ctx.Memory.TryRead(natInfoAddress, natInfo))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]);
if (size != NatInfoSize)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, typeof(long));
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
BinaryPrimitives.WriteInt32LittleEndian(natInfo[4..], 1);
BinaryPrimitives.WriteInt32LittleEndian(natInfo[8..], 3);
BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001);
return ctx.Memory.TryWrite(natInfoAddress, natInfo)
? ctx.SetReturn(0, typeof(long))
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -87,7 +87,7 @@ public static class NetCtlExports
LibraryName = "libSceNetCtl")]
public static int NetCtlCheckCallback(CpuContext ctx)
{
return ctx.SetReturn(0, typeof(long));
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -100,14 +100,14 @@ public static class NetCtlExports
var stateAddress = ctx[CpuRegister.Rdi];
if (stateAddress == 0)
{
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
Span<byte> stateBytes = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0);
return ctx.Memory.TryWrite(stateAddress, stateBytes)
? ctx.SetReturn(0, typeof(long))
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -122,7 +122,7 @@ public static class NetCtlExports
var callbackIdAddress = ctx[CpuRegister.Rdx];
if (function == 0 || callbackIdAddress == 0)
{
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
lock (CallbackGate)
@@ -130,20 +130,20 @@ public static class NetCtlExports
var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0);
if (callbackId < 0)
{
return ctx.SetReturn(NetCtlErrorNoSpace, typeof(long));
return SetReturn(ctx, NetCtlErrorNoSpace);
}
Span<byte> callbackIdBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId));
if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
Callbacks[callbackId] = new CallbackRegistration(function, argument);
}
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -157,7 +157,7 @@ public static class NetCtlExports
var infoAddress = ctx[CpuRegister.Rsi];
if (infoAddress == 0)
{
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
return SetReturn(ctx, NetCtlErrorInvalidAddress);
}
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),
_ => ctx.SetReturn(NetCtlErrorNotConnected, typeof(long)),
_ => SetReturn(ctx, NetCtlErrorNotConnected),
};
}
@@ -185,8 +185,8 @@ public static class NetCtlExports
{
Span<byte> bytes = stackalloc byte[count];
return ctx.Memory.TryWrite(address, bytes)
? ctx.SetReturn(0, typeof(long))
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
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)
? ctx.SetReturn(0, typeof(long))
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
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)
? ctx.SetReturn(0, typeof(long))
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int WriteAsciiZ(CpuContext ctx, ulong address, string value, int byteCount)
@@ -217,7 +217,13 @@ public static class NetCtlExports
}
return ctx.Memory.TryWrite(address, bytes)
? ctx.SetReturn(0, typeof(long))
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
? 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;
}
}
+20 -12
View File
@@ -1,9 +1,11 @@
// 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;
@@ -33,7 +35,7 @@ public static class NetExports
{
_initialized = true;
TraceNet("init", 0, 0, 0, 0);
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -47,7 +49,7 @@ public static class NetExports
_pools.Clear();
_resolvers.Clear();
TraceNet("term", 0, 0, 0, 0);
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -63,7 +65,7 @@ public static class NetExports
if (size <= 0)
{
return ctx.SetReturn(NetErrorInvalidArgument);
return SetReturn(ctx, NetErrorInvalidArgument);
}
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
@@ -88,11 +90,11 @@ public static class NetExports
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_pools.TryRemove(id, out _))
{
return ctx.SetReturn(NetErrorBadFileDescriptor);
return SetReturn(ctx, NetErrorBadFileDescriptor);
}
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -107,7 +109,7 @@ public static class NetExports
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
if (flags != 0)
{
return ctx.SetReturn(NetErrorInvalidArgument);
return SetReturn(ctx, NetErrorInvalidArgument);
}
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
@@ -129,8 +131,8 @@ public static class NetExports
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
return _resolvers.TryRemove(id, out _)
? ctx.SetReturn(0)
: ctx.SetReturn(NetErrorBadFileDescriptor);
? SetReturn(ctx, 0)
: SetReturn(ctx, NetErrorBadFileDescriptor);
}
[SysAbiExport(
@@ -144,19 +146,25 @@ public static class NetExports
var statusAddress = ctx[CpuRegister.Rsi];
if (statusAddress == 0)
{
return ctx.SetReturn(NetErrorInvalidArgument);
return SetReturn(ctx, NetErrorInvalidArgument);
}
if (!_resolvers.TryGetValue(id, out var resolver))
{
return ctx.SetReturn(NetErrorBadFileDescriptor);
return SetReturn(ctx, NetErrorBadFileDescriptor);
}
Span<byte> status = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
return ctx.Memory.TryWrite(statusAddress, status)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
+12 -4
View File
@@ -1,7 +1,9 @@
// 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;
@@ -26,7 +28,7 @@ public static class SslExports
var poolSize = ctx[CpuRegister.Rdi];
if (poolSize == 0)
{
return ctx.SetReturn(SslErrorOutOfSize);
return SetReturn(ctx, SslErrorOutOfSize);
}
var id = Interlocked.Increment(ref _nextContextId);
@@ -47,11 +49,11 @@ public static class SslExports
var id = unchecked((int)ctx[CpuRegister.Rdi]);
if (!_contexts.TryRemove(id, out _))
{
return ctx.SetReturn(SslErrorInvalidId);
return SetReturn(ctx, SslErrorInvalidId);
}
TraceSsl("term", id, 0);
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -63,7 +65,13 @@ public static class SslExports
{
var id = unchecked((int)ctx[CpuRegister.Rdi]);
TraceSsl("close", id, 0);
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceSsl(string operation, int id, ulong arg0)
-316
View File
@@ -1,316 +0,0 @@
// 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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -48,14 +48,20 @@ public static class NpEntitlementAccessExports
emptyList.Clear();
if (!ctx.Memory.TryWrite(listAddress, emptyList))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
TraceNpEntitlementAccess(
$"get_addcont_info_list service=0x{ctx[CpuRegister.Rdi]:X16} list=0x{listAddress:X16} " +
$"max={ctx[CpuRegister.Rdx]} flags=0x{ctx[CpuRegister.Rcx]:X16} -> empty");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
private static void TraceNpEntitlementAccess(string message)
+12 -6
View File
@@ -76,14 +76,14 @@ public static class NpManagerExports
var stateAddress = ctx[CpuRegister.Rsi];
if (stateAddress == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> stateBytes = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(stateBytes, 1);
return ctx.Memory.TryWrite(stateAddress, stateBytes)
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
: SetReturn(ctx, 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 ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> titleId = stackalloc byte[NpTitleIdSize];
@@ -105,11 +105,17 @@ public static class NpManagerExports
if (!ctx.Memory.TryRead(titleIdAddress, titleId) ||
!ctx.Memory.TryRead(titleSecretAddress, titleSecret))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceNp($"set_np_title_id title='{ReadTitleId(titleId)}'");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
@@ -3,6 +3,7 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.Np;
@@ -21,13 +22,13 @@ public static class NpUniversalDataSystemExports
var parameterAddress = ctx[CpuRegister.Rdi];
if (parameterAddress == 0)
{
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
return SetReturn(ctx, NpUniversalDataSystemErrorInvalidArgument);
}
Span<byte> parameters = stackalloc byte[16];
return ctx.Memory.TryRead(parameterAddress, parameters)
? ctx.SetReturn(0, typeof(long))
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -40,14 +41,14 @@ public static class NpUniversalDataSystemExports
var contextAddress = ctx[CpuRegister.Rdi];
if (contextAddress == 0)
{
return ctx.SetReturn(0, typeof(long));
return SetReturn(ctx, 0);
}
Span<byte> context = stackalloc byte[sizeof(int)];
BinaryPrimitives.WriteInt32LittleEndian(context, 1);
return ctx.Memory.TryWrite(contextAddress, context)
? ctx.SetReturn(0, typeof(long))
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -58,13 +59,13 @@ public static class NpUniversalDataSystemExports
public static int NpUniversalDataSystemCreateHandle(CpuContext ctx)
{
var handle = Interlocked.Increment(ref _nextHandle);
if (ctx.TryWriteInt32(ctx[CpuRegister.Rdi], handle, checkNil: true) ||
ctx.TryWriteInt32(ctx[CpuRegister.Rsi], handle, checkNil: true))
if (TryWriteInt32(ctx, ctx[CpuRegister.Rdi], handle) ||
TryWriteInt32(ctx, ctx[CpuRegister.Rsi], handle))
{
return ctx.SetReturn(0, typeof(long));
return SetReturn(ctx, 0);
}
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -74,6 +75,24 @@ public static class NpUniversalDataSystemExports
LibraryName = "libSceNpUniversalDataSystem")]
public static int NpUniversalDataSystemRegisterContext(CpuContext ctx)
{
return ctx.SetReturn(0, typeof(long));
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;
}
}
+11 -3
View File
@@ -1,6 +1,8 @@
// 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;
@@ -23,12 +25,12 @@ public static class NpWebApi2Exports
if (httpContextId <= 0 || poolSize == 0)
{
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
return SetReturn(ctx, NpWebApi2ErrorInvalidArgument);
}
Interlocked.Exchange(ref _initialized, 1);
TraceNpWebApi2("init", httpContextId, poolSize);
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -41,7 +43,13 @@ public static class NpWebApi2Exports
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
Interlocked.Exchange(ref _initialized, 0);
TraceNpWebApi2("term", libraryContextId, 0);
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
private static void TraceNpWebApi2(string operation, int id, ulong arg0)
-470
View File
@@ -1,470 +0,0 @@
// 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,
};
}
-136
View File
@@ -1,136 +0,0 @@
// 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;
}
}
+26 -198
View File
@@ -4,7 +4,6 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Runtime.InteropServices;
namespace SharpEmu.Libs.Pad;
@@ -30,8 +29,7 @@ public static class PadExports
public static int PadInit(CpuContext ctx)
{
_initialized = true;
DualSenseReader.EnsureStarted();
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
[SysAbiExport(
@@ -47,24 +45,20 @@ public static class PadExports
var parameterAddress = ctx[CpuRegister.Rcx];
if (!_initialized)
{
return ctx.SetReturn(OrbisPadErrorNotInitialized);
return SetReturn(ctx, OrbisPadErrorNotInitialized);
}
if (userId == -1)
{
return ctx.SetReturn(OrbisPadErrorDeviceNoHandle);
return SetReturn(ctx, OrbisPadErrorDeviceNoHandle);
}
if (userId != PrimaryUserId || type != StandardPortType || index != 0 || parameterAddress != 0)
{
return ctx.SetReturn(OrbisPadErrorDeviceNotConnected);
return SetReturn(ctx, OrbisPadErrorDeviceNotConnected);
}
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);
return SetReturn(ctx, PrimaryPadHandle);
}
[SysAbiExport(
@@ -76,8 +70,8 @@ public static class PadExports
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
return handle == PrimaryPadHandle
? ctx.SetReturn(0)
: ctx.SetReturn(OrbisPadErrorInvalidHandle);
? SetReturn(ctx, 0)
: SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
[SysAbiExport(
@@ -91,12 +85,12 @@ public static class PadExports
var informationAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
if (informationAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Span<byte> information = stackalloc byte[ControllerInformationSize];
@@ -111,8 +105,8 @@ public static class PadExports
BinaryPrimitives.WriteInt32LittleEndian(information[0x10..], 0);
return ctx.Memory.TryWrite(informationAddress, information)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -126,17 +120,17 @@ public static class PadExports
var dataAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
if (dataAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return WriteNeutralPadData(ctx, dataAddress)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -151,139 +145,27 @@ public static class PadExports
var count = unchecked((int)ctx[CpuRegister.Rdx]);
if (handle != PrimaryPadHandle)
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
return SetReturn(ctx, OrbisPadErrorInvalidHandle);
}
if (dataAddress == 0 || count < 1 || count > 64)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
return WriteNeutralPadData(ctx, dataAddress)
? ctx.SetReturn(1)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
Nid = "W2G-yoyMF5U",
ExportName = "scePadSetVibrationMode",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetVibrationMode(CpuContext ctx)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "yFVnOdGxvZY",
ExportName = "scePadSetVibration",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetVibration(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (parameterAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// ScePadVibrationParam: { uint8_t largeMotor; uint8_t smallMotor; }
Span<byte> parameter = stackalloc byte[2];
if (!ctx.Memory.TryRead(parameterAddress, parameter))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
DualSenseReader.SetRumble(parameter[0], parameter[1]);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "RR4novUEENY",
ExportName = "scePadSetLightBar",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadSetLightBar(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var parameterAddress = ctx[CpuRegister.Rsi];
if (handle != PrimaryPadHandle)
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
if (parameterAddress == 0)
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// ScePadColor: { uint8_t r; uint8_t g; uint8_t b; uint8_t reserved; }
Span<byte> color = stackalloc byte[4];
if (!ctx.Memory.TryRead(parameterAddress, color))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
DualSenseReader.SetLightbar(color[0], color[1], color[2]);
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "DscD1i9HX1w",
ExportName = "scePadResetLightBar",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libScePad")]
public static int PadResetLightBar(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
if (handle != PrimaryPadHandle)
{
return ctx.SetReturn(OrbisPadErrorInvalidHandle);
}
DualSenseReader.ResetLightbar();
return ctx.SetReturn(0);
? SetReturn(ctx, 1)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
{
Span<byte> data = stackalloc byte[PadDataSize];
data.Clear();
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;
data[0x04] = 128;
data[0x05] = 128;
data[0x06] = 128;
data[0x07] = 128;
BinaryPrimitives.WriteSingleLittleEndian(data[0x18..], 1.0f);
data[0x4C] = 1;
var timestampTicks = Stopwatch.GetTimestamp();
@@ -298,63 +180,9 @@ public static class PadExports
return ctx.Memory.TryWrite(dataAddress, data);
}
[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()
private static int SetReturn(CpuContext ctx, int 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;
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
+49 -8
View File
@@ -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 ctx.TryWriteUInt32(outEntries, availableEntries)
return TryWriteUInt32(ctx, 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 (!ctx.TryWriteUInt16(outChunkIdList + (i * sizeof(ushort)), chunkId))
if (!TryWriteUInt16(ctx, outChunkIdList + (i * sizeof(ushort)), chunkId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
}
TracePlayGo($"get_chunk_id write requested={numberOfEntries} wrote={entriesToWrite} available={availableEntries}");
return ctx.TryWriteUInt32(outEntries, entriesToWrite)
return TryWriteUInt32(ctx, 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
: ctx.TryWriteInt64(outEta, 0)
: TryWriteInt64(ctx, 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 ctx.TryWriteInt32(outSpeed, speed)
return TryWriteInt32(ctx, 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 (!ctx.TryReadUInt16(chunkIds + (i * sizeof(ushort)), out var chunkId))
if (!TryReadUInt16(ctx, 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 ctx.TryWriteUInt32(outEntries, 0)
return TryWriteUInt32(ctx, 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 (!ctx.TryReadUInt16(chunkIds + (i * sizeof(ushort)), out var chunkId))
if (!TryReadUInt16(ctx, chunkIds + (i * sizeof(ushort)), out var chunkId))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
@@ -728,6 +728,47 @@ 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,6 +3,7 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Threading;
namespace SharpEmu.Libs.SaveData;
@@ -34,10 +35,10 @@ public static class SaveDataDialogExports
{
if (Interlocked.CompareExchange(ref _status, StatusInitialized, StatusNone) != StatusNone)
{
return ctx.SetReturn(ErrorAlreadyInitialized);
return SetReturn(ctx, ErrorAlreadyInitialized);
}
return ctx.SetReturn(ErrorOk);
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
@@ -50,22 +51,22 @@ public static class SaveDataDialogExports
var paramAddress = ctx[CpuRegister.Rdi];
if (paramAddress == 0)
{
return ctx.SetReturn(ErrorArgNull);
return SetReturn(ctx, ErrorArgNull);
}
if (_status is not (StatusInitialized or StatusFinished))
{
return ctx.SetReturn(ErrorNotInitialized);
return SetReturn(ctx, ErrorNotInitialized);
}
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
_lastUserData = ctx.TryReadUInt64(paramAddress + 0xC8, out var userData) ? userData : 0;
_lastUserData = TryReadUInt64(ctx, paramAddress + 0xC8, out var userData) ? userData : 0;
// There is no host save dialog yet. Complete immediately with OK so
// guest polling sees a finished dialog instead of spinning forever.
Interlocked.Exchange(ref _status, StatusFinished);
TraceSaveDataDialog($"open mode={_lastMode} userData=0x{_lastUserData:X16} -> finished");
return ctx.SetReturn(ErrorOk);
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
@@ -73,21 +74,21 @@ public static class SaveDataDialogExports
ExportName = "sceSaveDataDialogGetStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogGetStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
public static int SaveDataDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "KK3Bdg1RWK0",
ExportName = "sceSaveDataDialogUpdateStatus",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => ctx.SetReturn(Volatile.Read(ref _status));
public static int SaveDataDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
[SysAbiExport(
Nid = "en7gNVnh878",
ExportName = "sceSaveDataDialogIsReadyToDisplay",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => ctx.SetReturn(1);
public static int SaveDataDialogIsReadyToDisplay(CpuContext ctx) => SetReturn(ctx, 1);
[SysAbiExport(
Nid = "yEiJ-qqr6Cg",
@@ -99,12 +100,12 @@ public static class SaveDataDialogExports
var resultAddress = ctx[CpuRegister.Rdi];
if (resultAddress == 0)
{
return ctx.SetReturn(ErrorArgNull);
return SetReturn(ctx, ErrorArgNull);
}
if (Volatile.Read(ref _status) != StatusFinished)
{
return ctx.SetReturn(ErrorNotFinished);
return SetReturn(ctx, ErrorNotFinished);
}
Span<byte> result = stackalloc byte[ResultSize];
@@ -116,10 +117,10 @@ public static class SaveDataDialogExports
if (!ctx.Memory.TryWrite(resultAddress, result))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return ctx.SetReturn(ErrorOk);
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
@@ -131,10 +132,10 @@ public static class SaveDataDialogExports
{
if (Interlocked.CompareExchange(ref _status, StatusFinished, StatusRunning) != StatusRunning)
{
return ctx.SetReturn(ErrorNotRunning);
return SetReturn(ctx, ErrorNotRunning);
}
return ctx.SetReturn(ErrorOk);
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
@@ -146,12 +147,12 @@ public static class SaveDataDialogExports
{
if (Interlocked.Exchange(ref _status, StatusNone) == StatusNone)
{
return ctx.SetReturn(ErrorNotInitialized);
return SetReturn(ctx, ErrorNotInitialized);
}
_lastMode = 0;
_lastUserData = 0;
return ctx.SetReturn(ErrorOk);
return SetReturn(ctx, ErrorOk);
}
[SysAbiExport(
@@ -159,14 +160,14 @@ public static class SaveDataDialogExports
ExportName = "sceSaveDataDialogProgressBarInc",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => ctx.SetReturn(ErrorOk);
public static int SaveDataDialogProgressBarInc(CpuContext ctx) => SetReturn(ctx, ErrorOk);
[SysAbiExport(
Nid = "hay1CfTmLyA",
ExportName = "sceSaveDataDialogProgressBarSetValue",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveDataDialog")]
public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => ctx.SetReturn(ErrorOk);
public static int SaveDataDialogProgressBarSetValue(CpuContext ctx) => SetReturn(ctx, ErrorOk);
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
{
@@ -181,6 +182,25 @@ 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))
+74 -65
View File
@@ -50,15 +50,15 @@ public static class SaveDataExports
try
{
Directory.CreateDirectory(ResolveSaveDataRoot());
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
catch (IOException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (UnauthorizedAccessException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
}
@@ -73,18 +73,18 @@ public static class SaveDataExports
var resultAddress = ctx[CpuRegister.Rsi];
if (condAddress == 0 || resultAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
if (!TryReadSearchCond(ctx, condAddress, out var cond) ||
!TryReadSearchResult(ctx, resultAddress, out var result))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (cond.UserId < 0 || cond.SortKey > SortKeyFreeBlocks || cond.SortOrder > SortOrderDescent)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
try
@@ -96,7 +96,7 @@ public static class SaveDataExports
}
else if (!TryReadFixedAscii(ctx, cond.TitleIdAddress, SaveDataTitleIdSize, out titleId))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
var root = ResolveTitleSaveRoot(cond.UserId, titleId);
@@ -108,21 +108,21 @@ public static class SaveDataExports
var setNum = result.DirNamesNum == 0
? 0
: Math.Min(result.DirNamesNum, entries.Count);
if (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
!ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum)))
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (setNum == 0)
{
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set=0 root='{root}'");
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
if (result.DirNamesAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
for (var i = 0; i < setNum; i++)
@@ -138,20 +138,20 @@ public static class SaveDataExports
(result.InfosAddress != 0 &&
!TryWriteSearchInfo(ctx, result.InfosAddress + ((ulong)i * SaveDataSearchInfoSize), entry)))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
}
TraceSaveData($"dir_name_search user={cond.UserId} title={titleId} hits={entries.Count} set={setNum} root='{root}'");
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
catch (IOException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (UnauthorizedAccessException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
}
@@ -166,25 +166,25 @@ public static class SaveDataExports
var resultAddress = ctx[CpuRegister.Rsi];
if (mountAddress == 0 || resultAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
if (!ctx.TryReadInt32(mountAddress, out var userId) ||
if (!TryReadInt32(ctx, 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) ||
!TryReadUInt32(ctx, mountAddress + 0x20, out var mountMode) ||
!TryReadUInt32(ctx, mountAddress + 0x24, out var resource) ||
!TryReadUInt32(ctx, mountAddress + 0x28, out var mode) ||
dirNameAddress == 0 ||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
try
@@ -199,12 +199,12 @@ public static class SaveDataExports
if (!existed && !create && !createIfMissing)
{
return ctx.SetReturn(OrbisSaveDataErrorNotFound);
return SetReturn(ctx, OrbisSaveDataErrorNotFound);
}
if (existed && create)
{
return ctx.SetReturn(OrbisSaveDataErrorExists);
return SetReturn(ctx, OrbisSaveDataErrorExists);
}
if (!existed)
@@ -221,67 +221,37 @@ public static class SaveDataExports
BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u);
if (!ctx.Memory.TryWrite(resultAddress, result))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, (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);
return SetReturn(ctx, 0);
}
catch (IOException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (UnauthorizedAccessException)
{
return ctx.SetReturn(OrbisSaveDataErrorInternal);
return SetReturn(ctx, OrbisSaveDataErrorInternal);
}
catch (ArgumentException)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
return SetReturn(ctx, OrbisSaveDataErrorParameter);
}
}
private static int _nextTransactionResource;
[SysAbiExport(
Nid = "gjRZNnw0JPE",
ExportName = "sceSaveDataCreateTransactionResource",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceSaveData")]
public static int SaveDataCreateTransactionResource(CpuContext ctx)
{
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
var reserved = ctx[CpuRegister.Rsi];
var resourceAddress = ctx[CpuRegister.Rdx];
if (resourceAddress == 0)
{
return ctx.SetReturn(OrbisSaveDataErrorParameter);
}
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
if (!ctx.TryWriteUInt32(resourceAddress, id))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
TraceSaveData(
$"create_transaction_resource user={userId} reserved=0x{reserved:X} resource_addr=0x{resourceAddress:X} id={id}");
return ctx.SetReturn(0);
}
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
{
cond = default;
if (!ctx.TryReadInt32(address, out var userId) ||
if (!TryReadInt32(ctx, address, out var userId) ||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
!ctx.TryReadUInt32(address + 0x18, out var sortKey) ||
!ctx.TryReadUInt32(address + 0x1C, out var sortOrder))
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
{
return false;
}
@@ -304,7 +274,7 @@ public static class SaveDataExports
{
result = default;
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
!ctx.TryReadUInt32(address + ResultDirNamesNumOffset, out var dirNamesNum) ||
!TryReadUInt32(ctx, address + ResultDirNamesNumOffset, out var dirNamesNum) ||
!ctx.TryReadUInt64(address + ResultParamsOffset, out var paramsAddress) ||
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
{
@@ -430,7 +400,7 @@ public static class SaveDataExports
{
var configured = Environment.GetEnvironmentVariable("SHARPEMU_SAVEDATA_DIR");
var root = string.IsNullOrWhiteSpace(configured)
? Path.Combine(AppContext.BaseDirectory, "user", "savedata")
? Path.Combine(Environment.CurrentDirectory, "user", "savedata")
: configured;
return Path.GetFullPath(root);
}
@@ -505,6 +475,45 @@ 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))
+12 -5
View File
@@ -2,6 +2,7 @@
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Libs.Share;
@@ -25,13 +26,13 @@ public static class ShareExports
var affinityMask = ctx[CpuRegister.Rdx];
if (memorySize == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
Interlocked.Exchange(ref _initialized, 1);
TraceShare($"initialize memory=0x{memorySize:X} priority={priority} affinity=0x{affinityMask:X}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
@@ -44,12 +45,12 @@ public static class ShareExports
var contentParamAddress = ctx[CpuRegister.Rdi];
if (contentParamAddress == 0)
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
if (!TryReadNullTerminatedUtf8(ctx, contentParamAddress, MaxContentParamBytes, out var contentParam))
{
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
_contentParam = contentParam;
@@ -59,7 +60,13 @@ public static class ShareExports
}
TraceShare($"set_content_param len={contentParam.Length} preview='{FormatTraceString(contentParam)}'");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
}
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
return (int)result;
}
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
-5
View File
@@ -10,15 +10,10 @@ 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 ctx.SetReturn(OrbisSystemServiceErrorParameter);
return SetReturn(ctx, 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)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (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 ctx.SetReturn(OrbisSystemServiceErrorParameter);
return SetReturn(ctx, OrbisSystemServiceErrorParameter);
}
Span<byte> status = stackalloc byte[SystemServiceStatusSize];
@@ -60,8 +60,8 @@ public static class SystemServiceExports
status[0x06] = 1;
return ctx.Memory.TryWrite(statusAddress, status)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (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 ctx.SetReturn(OrbisSystemServiceErrorParameter);
return SetReturn(ctx, 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)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -94,6 +94,12 @@ public static class SystemServiceExports
public static int SystemServiceHideSplashScreen(CpuContext ctx)
{
VulkanVideoPresenter.HideSplashScreen();
return ctx.SetReturn(0);
return SetReturn(ctx, 0);
}
private static int SetReturn(CpuContext ctx, int result)
{
ctx[CpuRegister.Rax] = unchecked((ulong)result);
return result;
}
}
@@ -4,6 +4,7 @@
using SharpEmu.HLE;
using System.Buffers.Binary;
using System.Text;
using System.Threading;
namespace SharpEmu.Libs.UserService;
@@ -39,12 +40,12 @@ public static class UserServiceExports
var userIdAddress = ctx[CpuRegister.Rdi];
if (userIdAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
return ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
return TryWriteInt32(ctx, userIdAddress, PrimaryUserId)
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -57,7 +58,7 @@ public static class UserServiceExports
var userIdListAddress = ctx[CpuRegister.Rdi];
if (userIdListAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
Span<byte> userIds = stackalloc byte[sizeof(int) * 4];
@@ -66,8 +67,8 @@ public static class UserServiceExports
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x08..], InvalidUserId);
BinaryPrimitives.WriteInt32LittleEndian(userIds[0x0C..], InvalidUserId);
return ctx.Memory.TryWrite(userIdListAddress, userIds)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -80,20 +81,20 @@ public static class UserServiceExports
var eventAddress = ctx[CpuRegister.Rdi];
if (eventAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
if (Interlocked.Exchange(ref _loginEventDelivered, 1) != 0)
{
return ctx.SetReturn(OrbisUserServiceErrorNoEvent);
return SetReturn(ctx, OrbisUserServiceErrorNoEvent);
}
Span<byte> payload = stackalloc byte[sizeof(int) * 2];
BinaryPrimitives.WriteInt32LittleEndian(payload[0..], 0);
BinaryPrimitives.WriteInt32LittleEndian(payload[sizeof(int)..], PrimaryUserId);
return ctx.Memory.TryWrite(eventAddress, payload)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -108,25 +109,25 @@ public static class UserServiceExports
var capacity = ctx[CpuRegister.Rdx];
if (userId != PrimaryUserId)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
if (nameAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
var nameBytes = Encoding.UTF8.GetBytes(PrimaryUserName);
if (capacity <= (ulong)nameBytes.Length)
{
return ctx.SetReturn(OrbisUserServiceErrorBufferTooShort);
return SetReturn(ctx, OrbisUserServiceErrorBufferTooShort);
}
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
nameBytes.CopyTo(output);
return ctx.Memory.TryWrite(nameAddress, output)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
? SetReturn(ctx, 0)
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
[SysAbiExport(
@@ -140,16 +141,29 @@ public static class UserServiceExports
var valueAddress = ctx[CpuRegister.Rsi];
if (parameterId != 1000)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidParameter);
return SetReturn(ctx, OrbisUserServiceErrorInvalidParameter);
}
if (valueAddress == 0)
{
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
return SetReturn(ctx, OrbisUserServiceErrorInvalidArgument);
}
return ctx.TryWriteInt32(valueAddress, 0)
? ctx.SetReturn(0)
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
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;
}
}
+23 -72
View File
@@ -4,7 +4,6 @@
using SharpEmu.HLE;
using SharpEmu.Libs.Kernel;
using System.Buffers.Binary;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.Threading;
@@ -49,13 +48,6 @@ 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)
{
@@ -368,7 +360,7 @@ public static class VideoOutExports
var bufferIndex = unchecked((int)ctx[CpuRegister.Rsi]);
var flipMode = unchecked((int)ctx[CpuRegister.Rdx]);
var flipArg = unchecked((long)ctx[CpuRegister.Rcx]);
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg, submitGpuImage: true);
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
}
[SysAbiExport(
@@ -448,7 +440,7 @@ public static class VideoOutExports
}
public static int SubmitFlipFromAgc(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg) =>
SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg, submitGpuImage: false);
SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg);
internal static void SubmitHostRgbaFrame(ReadOnlySpan<byte> rgbaFrame, uint width, uint height)
{
@@ -756,13 +748,7 @@ public static class VideoOutExports
return groupIndex < 0 ? groupIndex : setIndex;
}
private static int SubmitFlip(
CpuContext ctx,
int handle,
int bufferIndex,
int flipMode,
long flipArg,
bool submitGpuImage)
private static int SubmitFlip(CpuContext ctx, int handle, int bufferIndex, int flipMode, long flipArg)
{
if (!TryGetPort(handle, out var port))
{
@@ -790,17 +776,6 @@ public static class VideoOutExports
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
}
if (submitGpuImage &&
bufferIndex >= 0 &&
TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer))
{
_ = VulkanVideoPresenter.TrySubmitGuestImage(
displayBuffer.Address,
displayBuffer.Width,
displayBuffer.Height,
displayBuffer.PitchInPixel);
}
if (string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DUMP_VIDEOOUT"),
"1",
@@ -820,46 +795,9 @@ 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)
@@ -922,9 +860,9 @@ public static class VideoOutExports
private static bool TryReadBufferAttribute(CpuContext ctx, ulong attributeAddress, bool attribute2, out BufferAttribute attribute)
{
attribute = default;
if (!ctx.TryReadUInt32(attributeAddress + 0x04, out var tilingMode) ||
!ctx.TryReadUInt32(attributeAddress + 0x0C, out var width) ||
!ctx.TryReadUInt32(attributeAddress + 0x10, out var height))
if (!TryReadUInt32(ctx, attributeAddress + 0x04, out var tilingMode) ||
!TryReadUInt32(ctx, attributeAddress + 0x0C, out var width) ||
!TryReadUInt32(ctx, attributeAddress + 0x10, out var height))
{
return false;
}
@@ -941,10 +879,10 @@ public static class VideoOutExports
return true;
}
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))
if (!TryReadUInt32(ctx, attributeAddress + 0x00, out var pixelFormat32) ||
!TryReadUInt32(ctx, attributeAddress + 0x08, out var aspectRatio) ||
!TryReadUInt32(ctx, attributeAddress + 0x14, out var pitchInPixel) ||
!TryReadUInt32(ctx, attributeAddress + 0x18, out var option32))
{
return false;
}
@@ -1291,6 +1229,19 @@ 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
-10
View File
@@ -11,16 +11,6 @@
"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, )",
-68
View File
@@ -1,68 +0,0 @@
// 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
{
}
}
}
}
}
-115
View File
@@ -1,115 +0,0 @@
// 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,7 +4,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
</Project>
+4 -57
View File
@@ -12,7 +12,9 @@ public static class SharpEmuLog
new(StringComparer.Ordinal);
private static readonly object ConfigurationSync = new();
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
private static ISharpEmuLogSink _sink = ResolveSinkFromEnvironment();
private static ISharpEmuLogSink _sink = new ConsoleLogSink(
useColors: ResolveColorEnabledFromEnvironment(),
includeTimestamp: false);
public static LogLevel MinimumLevel
{
@@ -35,26 +37,11 @@ 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)
@@ -68,22 +55,6 @@ 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);
@@ -175,30 +146,6 @@ 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))