mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 35736e4740 | |||
| 92526ecacf |
+1
-1
@@ -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: 142 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 |
@@ -26,7 +26,6 @@ arm64/
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
.packages
|
||||
packages/
|
||||
*.nupkg
|
||||
.nuget/
|
||||
@@ -37,5 +36,3 @@ packages/
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
ehthumbs.db
|
||||
|
||||
.vs/
|
||||
|
||||
@@ -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>
|
||||
|
||||
+1
-3
@@ -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"
|
||||
|
||||
@@ -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
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"sdk": {
|
||||
"version": "10.0.103",
|
||||
"rollForward": "latestFeature"
|
||||
"rollForward": "disable"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
+1
-102
@@ -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,32 +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)
|
||||
{
|
||||
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;
|
||||
@@ -122,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;
|
||||
@@ -743,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);
|
||||
}
|
||||
|
||||
@@ -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,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",
|
||||
|
||||
@@ -415,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))
|
||||
@@ -456,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)
|
||||
{
|
||||
@@ -633,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)
|
||||
{
|
||||
@@ -694,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
|
||||
@@ -1222,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;
|
||||
}
|
||||
@@ -1239,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;
|
||||
|
||||
@@ -11,9 +11,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
{
|
||||
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;
|
||||
@@ -21,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;
|
||||
@@ -38,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);
|
||||
@@ -85,7 +81,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
_regions.Add(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
@@ -100,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;
|
||||
}
|
||||
|
||||
@@ -114,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)
|
||||
@@ -145,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)
|
||||
@@ -199,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
|
||||
@@ -216,7 +210,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
_regions.Add(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
@@ -233,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;
|
||||
}
|
||||
@@ -253,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++)
|
||||
{
|
||||
@@ -274,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;
|
||||
}
|
||||
|
||||
@@ -342,10 +334,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
_regions.Clear();
|
||||
_pageProtections.Clear();
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
_allocationSearchHints.Clear();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -402,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
|
||||
{
|
||||
@@ -485,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);
|
||||
@@ -549,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);
|
||||
@@ -609,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))
|
||||
{
|
||||
@@ -658,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))
|
||||
{
|
||||
@@ -721,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
|
||||
{
|
||||
@@ -736,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
|
||||
{
|
||||
@@ -746,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)
|
||||
@@ -805,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);
|
||||
@@ -829,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;
|
||||
@@ -1028,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;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[VMEM] {message}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
|
||||
@@ -543,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)
|
||||
@@ -555,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);
|
||||
@@ -565,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
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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, )",
|
||||
|
||||
@@ -1,118 +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>
|
||||
|
||||
<SolidColorBrush x:Key="BgBrush" Color="#0D1017" />
|
||||
<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" />
|
||||
</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="10" />
|
||||
<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="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="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>
|
||||
|
||||
<Style Selector="ListBox.library ListBoxItem">
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
<Setter Property="Margin" Value="4,2" />
|
||||
<Setter Property="Padding" Value="8,4" />
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
</Application>
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,22 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public sealed record GameEntry(string Name, string? TitleId, string Path, long SizeBytes)
|
||||
{
|
||||
public string Detail => TitleId is not null
|
||||
? $"{TitleId} • {FormatSize(SizeBytes)}"
|
||||
: $"{FormatSize(SizeBytes)} • {Path}";
|
||||
|
||||
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",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,64 +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();
|
||||
|
||||
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.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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);
|
||||
@@ -1,157 +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="800"
|
||||
MinWidth="980" MinHeight="620"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="{StaticResource BgBrush}"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="PreferSystemChrome"
|
||||
ExtendClientAreaTitleBarHeightHint="44"
|
||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico">
|
||||
|
||||
<Grid RowDefinitions="44,*,32">
|
||||
|
||||
<!-- Title bar -->
|
||||
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/logo.png" Width="20" Height="20" />
|
||||
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
<Border Classes="pill">
|
||||
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Main content -->
|
||||
<Grid Grid.Row="1" Margin="14" ColumnDefinitions="360,14,*">
|
||||
|
||||
<!-- Game library -->
|
||||
<Border Grid.Column="0" Classes="card" Padding="0">
|
||||
<Grid RowDefinitions="Auto,Auto,*,Auto">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto" Margin="16,16,16,10">
|
||||
<TextBlock Classes="sectionTitle" Text="GAME LIBRARY" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="1" Classes="pill">
|
||||
<TextBlock x:Name="GameCountText" Text="0" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<TextBox Grid.Row="1" x:Name="SearchBox" Watermark="Search games…" Margin="12,0,12,8" CornerRadius="8" />
|
||||
|
||||
<ListBox Grid.Row="2" x:Name="GameList" Classes="library" Background="Transparent" Margin="8,0">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Margin="4,4" Spacing="2">
|
||||
<TextBlock Text="{Binding Name}" FontSize="14" FontWeight="SemiBold" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<StackPanel Grid.Row="3" Orientation="Horizontal" Spacing="8" Margin="16,12,16,16">
|
||||
<Button x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder" />
|
||||
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" />
|
||||
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Right column -->
|
||||
<Grid Grid.Column="2" RowDefinitions="Auto,12,Auto,12,*">
|
||||
|
||||
<!-- Selected game / launch -->
|
||||
<Border Grid.Row="0" Classes="card">
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel Grid.Column="0" Spacing="6" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="20" FontWeight="Bold" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
||||
FontSize="12" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="8">
|
||||
<Ellipse x:Name="StatusDot" Width="9" Height="9" Fill="{StaticResource FaintBrush}" VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="StatusText" Text="Idle" FontSize="12" Foreground="{StaticResource MutedBrush}" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center" Margin="16,0,0,0">
|
||||
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
||||
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Launch options -->
|
||||
<Border Grid.Row="2" Classes="card">
|
||||
<StackPanel Spacing="12">
|
||||
<TextBlock Classes="sectionTitle" Text="LAUNCH OPTIONS" />
|
||||
<WrapPanel>
|
||||
<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>
|
||||
|
||||
<!-- Console -->
|
||||
<Border Grid.Row="4" Classes="card" Padding="0">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,14,16,10">
|
||||
<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,10,10">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
</Grid>
|
||||
</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>
|
||||
@@ -1,609 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Collections.ObjectModel;
|
||||
using System.Reflection;
|
||||
using System.Text.Json;
|
||||
using Avalonia.Controls;
|
||||
using Avalonia.Input;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Platform.Storage;
|
||||
using Avalonia.Threading;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public partial class MainWindow : Window
|
||||
{
|
||||
private const int MaxConsoleLines = 4000;
|
||||
|
||||
private static readonly IBrush DefaultLineBrush = new SolidColorBrush(Color.Parse("#C7CFDE"));
|
||||
private static readonly IBrush DimLineBrush = new SolidColorBrush(Color.Parse("#6B7488"));
|
||||
private static readonly IBrush InfoLineBrush = new SolidColorBrush(Color.Parse("#6FA8FF"));
|
||||
private static readonly IBrush WarningLineBrush = new SolidColorBrush(Color.Parse("#E8B341"));
|
||||
private static readonly IBrush ErrorLineBrush = new SolidColorBrush(Color.Parse("#F2777C"));
|
||||
private static readonly IBrush SuccessLineBrush = new SolidColorBrush(Color.Parse("#63D489"));
|
||||
|
||||
private readonly List<GameEntry> _allGames = new();
|
||||
private readonly ObservableCollection<GameEntry> _visibleGames = new();
|
||||
private readonly ObservableCollection<LogLine> _consoleLines = new();
|
||||
private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new();
|
||||
private readonly DispatcherTimer _consoleFlushTimer;
|
||||
|
||||
private GuiSettings _settings = new();
|
||||
private EmulatorProcess? _emulator;
|
||||
private string? _emulatorExePath;
|
||||
private bool _isRunning;
|
||||
private int _autoScrollTicks;
|
||||
|
||||
public MainWindow()
|
||||
{
|
||||
InitializeComponent();
|
||||
|
||||
GameList.ItemsSource = _visibleGames;
|
||||
ConsoleList.ItemsSource = _consoleLines;
|
||||
|
||||
_consoleFlushTimer = new DispatcherTimer
|
||||
{
|
||||
Interval = TimeSpan.FromMilliseconds(80),
|
||||
};
|
||||
_consoleFlushTimer.Tick += (_, _) =>
|
||||
{
|
||||
FlushPendingConsoleLines();
|
||||
MaybeAutoScroll();
|
||||
};
|
||||
_consoleFlushTimer.Start();
|
||||
|
||||
TitleBar.PointerPressed += OnTitleBarPointerPressed;
|
||||
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
||||
GameList.DoubleTapped += (_, _) => LaunchSelected();
|
||||
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
||||
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||
RescanButton.Click += async (_, _) => await RescanLibraryAsync();
|
||||
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
|
||||
LaunchButton.Click += (_, _) => LaunchSelected();
|
||||
StopButton.Click += (_, _) => _emulator?.Stop();
|
||||
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
|
||||
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
|
||||
|
||||
Opened += async (_, _) => await OnOpenedAsync();
|
||||
Closing += (_, _) => OnWindowClosing();
|
||||
}
|
||||
|
||||
private async Task OnOpenedAsync()
|
||||
{
|
||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||
if (version is not null)
|
||||
{
|
||||
VersionText.Text = $"v{version.ToString(3)}";
|
||||
}
|
||||
|
||||
_settings = GuiSettings.Load();
|
||||
ApplySettingsToControls();
|
||||
LocateEmulator();
|
||||
await RescanLibraryAsync();
|
||||
}
|
||||
|
||||
private void OnWindowClosing()
|
||||
{
|
||||
ReadControlsIntoSettings();
|
||||
_settings.Save();
|
||||
_consoleFlushTimer.Stop();
|
||||
_emulator?.Dispose();
|
||||
}
|
||||
|
||||
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||
{
|
||||
if (e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
|
||||
{
|
||||
BeginMoveDrag(e);
|
||||
}
|
||||
}
|
||||
|
||||
// ---- Settings <-> controls ----
|
||||
|
||||
private void ApplySettingsToControls()
|
||||
{
|
||||
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
|
||||
{
|
||||
"trace" => 0,
|
||||
"debug" => 1,
|
||||
"info" => 2,
|
||||
"warning" or "warn" => 3,
|
||||
"error" => 4,
|
||||
"critical" or "fatal" => 5,
|
||||
_ => 2,
|
||||
};
|
||||
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
||||
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
||||
}
|
||||
|
||||
private void ReadControlsIntoSettings()
|
||||
{
|
||||
_settings.LogLevel = SelectedLogLevel();
|
||||
_settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||
_settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||
}
|
||||
|
||||
private string SelectedLogLevel()
|
||||
{
|
||||
return LogLevelBox.SelectedIndex switch
|
||||
{
|
||||
0 => "Trace",
|
||||
1 => "Debug",
|
||||
2 => "Info",
|
||||
3 => "Warning",
|
||||
4 => "Error",
|
||||
5 => "Critical",
|
||||
_ => "Info",
|
||||
};
|
||||
}
|
||||
|
||||
// ---- Emulator discovery ----
|
||||
|
||||
private void LocateEmulator()
|
||||
{
|
||||
var exeName = OperatingSystem.IsWindows() ? "SharpEmu.exe" : "SharpEmu";
|
||||
var baseDirectory = AppContext.BaseDirectory;
|
||||
var candidates = new List<string>();
|
||||
if (!string.IsNullOrWhiteSpace(_settings.EmulatorPath))
|
||||
{
|
||||
candidates.Add(_settings.EmulatorPath);
|
||||
}
|
||||
|
||||
// The GUI and the CLI are the same executable: with arguments it runs
|
||||
// the emulator, so the preferred child process is this process itself.
|
||||
if (Environment.ProcessPath is { } selfPath &&
|
||||
Path.GetFileNameWithoutExtension(selfPath).Equals("SharpEmu", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
candidates.Add(selfPath);
|
||||
}
|
||||
|
||||
candidates.Add(Path.Combine(baseDirectory, exeName));
|
||||
candidates.Add(Path.Combine(baseDirectory, "win-x64", exeName));
|
||||
candidates.Add(Path.Combine(baseDirectory, "..", exeName));
|
||||
|
||||
_emulatorExePath = candidates.FirstOrDefault(File.Exists) is { } found
|
||||
? Path.GetFullPath(found)
|
||||
: null;
|
||||
|
||||
EmulatorPathText.Text = _emulatorExePath is not null
|
||||
? $"Emulator: {_emulatorExePath}"
|
||||
: "Emulator: SharpEmu executable not found — build SharpEmu.CLI first.";
|
||||
}
|
||||
|
||||
// ---- Game library ----
|
||||
|
||||
private async Task AddFolderAsync()
|
||||
{
|
||||
var folders = await StorageProvider.OpenFolderPickerAsync(new FolderPickerOpenOptions
|
||||
{
|
||||
Title = "Choose a folder containing games",
|
||||
AllowMultiple = false,
|
||||
});
|
||||
|
||||
var path = folders.FirstOrDefault()?.TryGetLocalPath();
|
||||
if (string.IsNullOrEmpty(path))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (!_settings.GameFolders.Contains(path, StringComparer.OrdinalIgnoreCase))
|
||||
{
|
||||
_settings.GameFolders.Add(path);
|
||||
_settings.Save();
|
||||
}
|
||||
|
||||
await RescanLibraryAsync();
|
||||
}
|
||||
|
||||
private async Task RescanLibraryAsync()
|
||||
{
|
||||
var folders = _settings.GameFolders.ToArray();
|
||||
StatusBarRight.Text = "Scanning library…";
|
||||
|
||||
var games = await Task.Run(() => ScanFolders(folders));
|
||||
|
||||
_allGames.Clear();
|
||||
_allGames.AddRange(games);
|
||||
RefreshVisibleGames();
|
||||
StatusBarRight.Text = folders.Length == 0
|
||||
? "Add a game folder to populate the library."
|
||||
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
|
||||
}
|
||||
|
||||
private static List<GameEntry> ScanFolders(IReadOnlyList<string> folders)
|
||||
{
|
||||
var games = new List<GameEntry>();
|
||||
var seen = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
|
||||
var enumeration = new EnumerationOptions
|
||||
{
|
||||
IgnoreInaccessible = true,
|
||||
RecurseSubdirectories = true,
|
||||
MaxRecursionDepth = 8,
|
||||
};
|
||||
|
||||
foreach (var folder in folders)
|
||||
{
|
||||
if (!Directory.Exists(folder))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(folder, "eboot.bin", enumeration))
|
||||
{
|
||||
var fullPath = Path.GetFullPath(file);
|
||||
if (!seen.Add(fullPath))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
long size = 0;
|
||||
try
|
||||
{
|
||||
size = new FileInfo(fullPath).Length;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
}
|
||||
|
||||
var (title, titleId) = TryReadParamJson(fullPath);
|
||||
games.Add(new GameEntry(title ?? GameNameFor(fullPath), titleId, fullPath, size));
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Skip folders that fail to enumerate.
|
||||
}
|
||||
}
|
||||
|
||||
games.Sort((a, b) => string.Compare(a.Name, b.Name, StringComparison.OrdinalIgnoreCase));
|
||||
return games;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Reads the game title and title id from sce_sys/param.json next to the
|
||||
/// executable, when present.
|
||||
/// </summary>
|
||||
private static (string? Title, string? TitleId) TryReadParamJson(string ebootPath)
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(ebootPath);
|
||||
if (directory is null)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
var paramPath = Path.Combine(directory, "sce_sys", "param.json");
|
||||
if (!File.Exists(paramPath))
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
|
||||
// ReadAllText handles a UTF-8 BOM, which JsonDocument rejects in
|
||||
// raw bytes.
|
||||
using var document = JsonDocument.Parse(File.ReadAllText(paramPath));
|
||||
var root = document.RootElement;
|
||||
|
||||
string? titleId = null;
|
||||
if (root.TryGetProperty("titleId", out var idElement) && idElement.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
titleId = idElement.GetString();
|
||||
}
|
||||
|
||||
string? title = null;
|
||||
if (root.TryGetProperty("localizedParameters", out var localized) &&
|
||||
localized.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (localized.TryGetProperty("defaultLanguage", out var language) &&
|
||||
language.ValueKind == JsonValueKind.String &&
|
||||
localized.TryGetProperty(language.GetString()!, out var defaultBlock) &&
|
||||
defaultBlock.ValueKind == JsonValueKind.Object &&
|
||||
defaultBlock.TryGetProperty("titleName", out var titleName) &&
|
||||
titleName.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
title = titleName.GetString();
|
||||
}
|
||||
else
|
||||
{
|
||||
foreach (var property in localized.EnumerateObject())
|
||||
{
|
||||
if (property.Value.ValueKind == JsonValueKind.Object &&
|
||||
property.Value.TryGetProperty("titleName", out var anyTitleName) &&
|
||||
anyTitleName.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
title = anyTitleName.GetString();
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
string.IsNullOrWhiteSpace(title) ? null : title,
|
||||
string.IsNullOrWhiteSpace(titleId) ? null : titleId);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return (null, null);
|
||||
}
|
||||
}
|
||||
|
||||
private static string GameNameFor(string ebootPath)
|
||||
{
|
||||
var directory = Path.GetDirectoryName(ebootPath);
|
||||
var name = directory is not null ? Path.GetFileName(directory) : null;
|
||||
return string.IsNullOrEmpty(name) ? Path.GetFileName(ebootPath) : name;
|
||||
}
|
||||
|
||||
private void RefreshVisibleGames()
|
||||
{
|
||||
var query = SearchBox.Text?.Trim() ?? string.Empty;
|
||||
var selected = GameList.SelectedItem as GameEntry;
|
||||
|
||||
_visibleGames.Clear();
|
||||
foreach (var game in _allGames)
|
||||
{
|
||||
if (query.Length == 0 ||
|
||||
game.Name.Contains(query, StringComparison.OrdinalIgnoreCase) ||
|
||||
game.Path.Contains(query, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
_visibleGames.Add(game);
|
||||
}
|
||||
}
|
||||
|
||||
GameCountText.Text = _visibleGames.Count.ToString();
|
||||
if (selected is not null && _visibleGames.Contains(selected))
|
||||
{
|
||||
GameList.SelectedItem = selected;
|
||||
}
|
||||
|
||||
UpdateSelectedGame();
|
||||
}
|
||||
|
||||
private void UpdateSelectedGame()
|
||||
{
|
||||
if (GameList.SelectedItem is GameEntry game)
|
||||
{
|
||||
SelectedGameTitle.Text = game.Name;
|
||||
SelectedGamePath.Text = game.Path;
|
||||
}
|
||||
else
|
||||
{
|
||||
SelectedGameTitle.Text = "No game selected";
|
||||
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
|
||||
}
|
||||
|
||||
UpdateRunButtons();
|
||||
}
|
||||
|
||||
// ---- Launching ----
|
||||
|
||||
private async Task OpenFileAsync()
|
||||
{
|
||||
var files = await StorageProvider.OpenFilePickerAsync(new FilePickerOpenOptions
|
||||
{
|
||||
Title = "Open an executable to launch",
|
||||
AllowMultiple = false,
|
||||
FileTypeFilter = new[]
|
||||
{
|
||||
new FilePickerFileType("PS executables") { Patterns = new[] { "eboot.bin", "*.bin", "*.self", "*.elf" } },
|
||||
FilePickerFileTypes.All,
|
||||
},
|
||||
});
|
||||
|
||||
var path = files.FirstOrDefault()?.TryGetLocalPath();
|
||||
if (!string.IsNullOrEmpty(path))
|
||||
{
|
||||
Launch(path, Path.GetFileName(path));
|
||||
}
|
||||
}
|
||||
|
||||
private void LaunchSelected()
|
||||
{
|
||||
if (GameList.SelectedItem is GameEntry game)
|
||||
{
|
||||
Launch(game.Path, game.Name);
|
||||
}
|
||||
}
|
||||
|
||||
private void Launch(string ebootPath, string displayName)
|
||||
{
|
||||
if (_isRunning)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (_emulatorExePath is null)
|
||||
{
|
||||
LocateEmulator();
|
||||
if (_emulatorExePath is null)
|
||||
{
|
||||
AppendConsoleLine("SharpEmu executable not found. Build the SharpEmu.CLI project first (dotnet build).", ErrorLineBrush);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
ReadControlsIntoSettings();
|
||||
_settings.Save();
|
||||
|
||||
var arguments = new List<string>
|
||||
{
|
||||
"--cpu-engine=native",
|
||||
$"--log-level={_settings.LogLevel.ToLowerInvariant()}",
|
||||
};
|
||||
if (_settings.StrictDynlibResolution)
|
||||
{
|
||||
arguments.Add("--strict");
|
||||
}
|
||||
|
||||
if (_settings.ImportTraceLimit > 0)
|
||||
{
|
||||
arguments.Add($"--trace-imports={_settings.ImportTraceLimit}");
|
||||
}
|
||||
|
||||
arguments.Add(ebootPath);
|
||||
|
||||
_consoleLines.Clear();
|
||||
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
|
||||
|
||||
var emulator = new EmulatorProcess();
|
||||
emulator.OutputReceived += (line, isError) => _pendingLines.Enqueue((line, isError));
|
||||
emulator.Exited += code => Dispatcher.UIThread.Post(() => OnEmulatorExited(code));
|
||||
|
||||
try
|
||||
{
|
||||
emulator.Start(_emulatorExePath, arguments, Path.GetDirectoryName(ebootPath));
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
emulator.Dispose();
|
||||
AppendConsoleLine($"Failed to start the emulator: {ex.Message}", ErrorLineBrush);
|
||||
return;
|
||||
}
|
||||
|
||||
_emulator = emulator;
|
||||
_isRunning = true;
|
||||
StatusDot.Fill = SuccessLineBrush;
|
||||
StatusText.Text = $"Running — {displayName}";
|
||||
StatusBarRight.Text = $"Running {displayName}";
|
||||
UpdateRunButtons();
|
||||
}
|
||||
|
||||
private void OnEmulatorExited(int exitCode)
|
||||
{
|
||||
FlushPendingConsoleLines();
|
||||
_isRunning = false;
|
||||
_emulator?.Dispose();
|
||||
_emulator = null;
|
||||
|
||||
var meaning = exitCode switch
|
||||
{
|
||||
0 => "OK",
|
||||
1 => "invalid arguments",
|
||||
2 => "eboot not found",
|
||||
3 => "runtime exception",
|
||||
4 => "emulation error",
|
||||
_ => "unknown",
|
||||
};
|
||||
var brush = exitCode == 0 ? SuccessLineBrush : ErrorLineBrush;
|
||||
AppendConsoleLine($"Process exited with code {exitCode} ({meaning}).", brush);
|
||||
|
||||
StatusDot.Fill = exitCode == 0 ? (IBrush)SuccessLineBrush : ErrorLineBrush;
|
||||
StatusText.Text = $"Exited with code {exitCode} ({meaning})";
|
||||
StatusBarRight.Text = "Idle";
|
||||
UpdateRunButtons();
|
||||
}
|
||||
|
||||
private void UpdateRunButtons()
|
||||
{
|
||||
LaunchButton.IsEnabled = !_isRunning && GameList.SelectedItem is GameEntry;
|
||||
StopButton.IsEnabled = _isRunning;
|
||||
OpenFileButton.IsEnabled = !_isRunning;
|
||||
}
|
||||
|
||||
// ---- Console ----
|
||||
|
||||
private void FlushPendingConsoleLines()
|
||||
{
|
||||
if (_pendingLines.IsEmpty)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var incoming = new List<LogLine>();
|
||||
while (_pendingLines.TryDequeue(out var pending))
|
||||
{
|
||||
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
|
||||
}
|
||||
|
||||
if (incoming.Count >= MaxConsoleLines)
|
||||
{
|
||||
// A burst larger than the cap: keep only the newest lines.
|
||||
_consoleLines.Clear();
|
||||
for (var i = incoming.Count - MaxConsoleLines; i < incoming.Count; i++)
|
||||
{
|
||||
_consoleLines.Add(incoming[i]);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
var overflow = _consoleLines.Count + incoming.Count - MaxConsoleLines;
|
||||
for (var i = 0; i < overflow; i++)
|
||||
{
|
||||
_consoleLines.RemoveAt(0);
|
||||
}
|
||||
|
||||
foreach (var line in incoming)
|
||||
{
|
||||
_consoleLines.Add(line);
|
||||
}
|
||||
}
|
||||
|
||||
_autoScrollTicks = 3;
|
||||
}
|
||||
|
||||
private void AppendConsoleLine(string text, IBrush brush)
|
||||
{
|
||||
_consoleLines.Add(new LogLine(text, brush));
|
||||
_autoScrollTicks = 3;
|
||||
MaybeAutoScroll();
|
||||
}
|
||||
|
||||
private void MaybeAutoScroll()
|
||||
{
|
||||
// ScrollToEnd is applied over a few flush-timer ticks because the
|
||||
// virtualizing panel re-estimates its extent after large batches, and
|
||||
// a single scroll can land short of the true end. A synchronous
|
||||
// ScrollIntoView during rapid adds is avoided entirely — it can crash
|
||||
// the panel with "Invalid Arrange rectangle".
|
||||
if (_autoScrollTicks <= 0 || AutoScrollCheck.IsChecked != true)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_autoScrollTicks--;
|
||||
(ConsoleList.Scroll as ScrollViewer)?.ScrollToEnd();
|
||||
}
|
||||
|
||||
private static IBrush BrushForLine(string line)
|
||||
{
|
||||
if (line.Contains("[ERROR]", StringComparison.Ordinal) ||
|
||||
line.Contains("[CRITICAL]", StringComparison.Ordinal))
|
||||
{
|
||||
return ErrorLineBrush;
|
||||
}
|
||||
|
||||
if (line.Contains("[WARNING]", StringComparison.Ordinal))
|
||||
{
|
||||
return WarningLineBrush;
|
||||
}
|
||||
|
||||
if (line.Contains("[INFO]", StringComparison.Ordinal))
|
||||
{
|
||||
return InfoLineBrush;
|
||||
}
|
||||
|
||||
if (line.Contains("[DEBUG]", StringComparison.Ordinal) ||
|
||||
line.Contains("[TRACE]", StringComparison.Ordinal))
|
||||
{
|
||||
return DimLineBrush;
|
||||
}
|
||||
|
||||
return DefaultLineBrush;
|
||||
}
|
||||
|
||||
private async Task CopyConsoleAsync()
|
||||
{
|
||||
if (_consoleLines.Count == 0 || Clipboard is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var text = string.Join(Environment.NewLine, _consoleLines.Select(line => line.Text));
|
||||
await Clipboard.SetTextAsync(text);
|
||||
}
|
||||
}
|
||||
@@ -1,28 +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\logo.png" Link="Assets/logo.png" />
|
||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -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=="
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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,39 +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 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 TryReadUInt64(ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
@@ -178,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];
|
||||
|
||||
@@ -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>
|
||||
|
||||
+364
-2879
File diff suppressed because it is too large
Load Diff
@@ -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,140 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
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 (!TryReadUInt16(ctx, userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||
!TryReadUInt16(ctx, userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||
!TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount) ||
|
||||
directResourceCount > MaxMetadataEntries)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var resourceCounts = new ushort[ResourceClassCount];
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
if (!TryReadUInt16(
|
||||
ctx,
|
||||
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 (!TryReadUInt16(ctx, 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 (!TryReadUInt16(
|
||||
ctx,
|
||||
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;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
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
@@ -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();
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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)
|
||||
|
||||
@@ -95,7 +95,7 @@ public static class AudioOut2Exports
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
||||
return TryWriteUInt64(ctx, outContextAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
@@ -125,7 +125,7 @@ public static class AudioOut2Exports
|
||||
|
||||
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
||||
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
||||
return ctx.TryWriteUInt64(outPortAddress, handle)
|
||||
return TryWriteUInt64(ctx, outPortAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
@@ -211,11 +211,18 @@ public static class AudioOut2Exports
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||
return ctx.TryWriteUInt64(outUserAddress, handle)
|
||||
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);
|
||||
|
||||
@@ -1,242 +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;
|
||||
using System.Threading;
|
||||
|
||||
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) => SetReturn(ctx, 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 SetReturn(ctx, (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 SetReturn(ctx, 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 SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
port.Dispose();
|
||||
return SetReturn(ctx, 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 SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (sourceAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
|
||||
try
|
||||
{
|
||||
var source = buffer.AsSpan(0, port.BufferByteLength);
|
||||
if (!ctx.Memory.TryRead(sourceAddress, source))
|
||||
{
|
||||
return SetReturn(ctx, (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 SetReturn(ctx, 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 SetReturn(
|
||||
ctx,
|
||||
Ports.ContainsKey(handle)
|
||||
? 0
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -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,77 +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 SetReturn(
|
||||
ctx,
|
||||
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 SetReturn(
|
||||
ctx,
|
||||
Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -112,7 +112,7 @@ public static class FiberExports
|
||||
return SetReturn(ctx, FiberErrorAlignment);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
||||
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
@@ -130,7 +130,7 @@ public static class FiberExports
|
||||
return SetReturn(ctx, error);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
||||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
@@ -143,7 +143,7 @@ public static class FiberExports
|
||||
_continuations.TryRemove(fiber, out _);
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
_stackRanges.TryRemove(fiber, out _);
|
||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
||||
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
@@ -240,7 +240,7 @@ public static class FiberExports
|
||||
|
||||
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 SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
@@ -264,7 +264,7 @@ 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 SetReturn(ctx, FiberErrorState);
|
||||
@@ -284,7 +284,7 @@ public static class FiberExports
|
||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
@@ -318,7 +318,7 @@ public static class FiberExports
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
||||
return TryWriteUInt64(ctx, outAddress, fiberAddress)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
@@ -342,7 +342,7 @@ public static class FiberExports
|
||||
return SetReturn(ctx, error);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
|
||||
if (!TryReadUInt64(ctx, info, out var size) || size != FiberInfoSize)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
@@ -352,12 +352,12 @@ public static class FiberExports
|
||||
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 SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
@@ -384,7 +384,7 @@ public static class FiberExports
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
@@ -441,7 +441,7 @@ public static class FiberExports
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
||||
return TryWriteUInt64(ctx, outAddress, ctx[CpuRegister.Rbp])
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
@@ -482,12 +482,12 @@ public static class FiberExports
|
||||
}
|
||||
|
||||
if (optParam != 0 &&
|
||||
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
@@ -497,25 +497,25 @@ 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 SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (contextAddress != 0)
|
||||
{
|
||||
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||
if (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
@@ -616,9 +616,9 @@ 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 SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
@@ -631,12 +631,12 @@ 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 SetReturn(ctx, FiberErrorInvalid);
|
||||
@@ -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,31 @@ 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);
|
||||
|
||||
@@ -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;
|
||||
@@ -54,7 +55,7 @@ public static class KernelEventFlagCompatExports
|
||||
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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
@@ -218,7 +219,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
uint timeoutUsec = 0;
|
||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
||||
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
@@ -233,7 +234,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
@@ -339,7 +340,7 @@ public static class KernelEventFlagCompatExports
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (waiterCountAddress != 0 &&
|
||||
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
@@ -452,6 +453,77 @@ 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;
|
||||
@@ -542,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}");
|
||||
}
|
||||
@@ -550,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}");
|
||||
}
|
||||
@@ -558,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;
|
||||
}
|
||||
}
|
||||
@@ -365,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);
|
||||
@@ -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;
|
||||
@@ -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;
|
||||
@@ -5867,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
|
||||
@@ -6199,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)
|
||||
|
||||
@@ -95,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",
|
||||
@@ -1018,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)
|
||||
{
|
||||
|
||||
@@ -22,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();
|
||||
@@ -32,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();
|
||||
|
||||
@@ -719,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 (!TryWriteInt32(ctx, 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",
|
||||
@@ -841,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",
|
||||
@@ -1049,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",
|
||||
@@ -1119,7 +1016,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
ctx[CpuRegister.Rax] = unchecked((uint)key);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
|
||||
@@ -7,6 +7,7 @@ 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;
|
||||
@@ -86,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
|
||||
{
|
||||
@@ -738,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))
|
||||
{
|
||||
@@ -754,7 +752,6 @@ public static class KernelRuntimeCompatExports
|
||||
_nextReservedVirtualBase = Math.Max(_nextReservedVirtualBase, mappedAddress + length);
|
||||
}
|
||||
|
||||
KernelMemoryCompatExports.RegisterReservedVirtualRange(mappedAddress, length);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1735,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)
|
||||
@@ -1757,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;
|
||||
@@ -47,7 +49,7 @@ public static class KernelSemaphoreCompatExports
|
||||
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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
@@ -66,7 +68,7 @@ public static class KernelSemaphoreCompatExports
|
||||
Count = initialCount,
|
||||
};
|
||||
|
||||
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
||||
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||
{
|
||||
_semaphores.TryRemove(handle, out _);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
@@ -108,12 +110,12 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
if (!ctx.TryReadUInt32(timeoutAddress, out _))
|
||||
if (!TryReadUInt32(ctx, timeoutAddress, out _))
|
||||
{
|
||||
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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
@@ -220,7 +222,7 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
@@ -256,6 +258,56 @@ public static class KernelSemaphoreCompatExports
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (address == 0 || maxLength <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[Math.Min(maxLength, 4096)];
|
||||
for (var i = 0; i < bytes.Length; i++)
|
||||
{
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address + (ulong)i, current))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, i);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes[i] = current[0];
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceSemaphore(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
@@ -1,322 +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;
|
||||
using System.Threading;
|
||||
|
||||
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 SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
|
||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (StateGate)
|
||||
{
|
||||
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 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 SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
|
||||
var rackHandles = Racks
|
||||
.Where(pair => pair.Value.SystemHandle == handle)
|
||||
.Select(pair => pair.Key)
|
||||
.ToArray();
|
||||
foreach (var rackHandle in rackHandles)
|
||||
{
|
||||
RemoveRackLocked(rackHandle);
|
||||
}
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 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 SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
}
|
||||
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
|
||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (StateGate)
|
||||
{
|
||||
Racks[handle] = new RackState(systemHandle, rackId);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 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 SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||
}
|
||||
|
||||
RemoveRackLocked(handle);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 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 SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||
}
|
||||
|
||||
var existing = Voices.FirstOrDefault(
|
||||
pair => pair.Value.RackHandle == rackHandle && pair.Value.VoiceIndex == voiceIndex);
|
||||
if (existing.Key != 0)
|
||||
{
|
||||
return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) ||
|
||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (StateGate)
|
||||
{
|
||||
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uu94irFOGpA",
|
||||
ExportName = "sceNgs2VoiceControl",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceControl(CpuContext ctx)
|
||||
{
|
||||
lock (StateGate)
|
||||
{
|
||||
return SetReturn(
|
||||
ctx,
|
||||
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 SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
}
|
||||
|
||||
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (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 SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (bufferAddress != 0 && bufferSize != 0)
|
||||
{
|
||||
if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize))
|
||||
{
|
||||
return SetReturn(ctx, (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 SetReturn(ctx, 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);
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -4,7 +4,6 @@
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
@@ -59,7 +58,6 @@ public static class PadExports
|
||||
return SetReturn(ctx, OrbisPadErrorDeviceNotConnected);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine("[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");
|
||||
return SetReturn(ctx, PrimaryPadHandle);
|
||||
}
|
||||
|
||||
@@ -160,33 +158,14 @@ public static class PadExports
|
||||
: SetReturn(ctx, (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 SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
|
||||
{
|
||||
Span<byte> data = stackalloc byte[PadDataSize];
|
||||
data.Clear();
|
||||
var acceptsKeyboardInput = IsEmulatorWindowFocused();
|
||||
var buttons = acceptsKeyboardInput ? ReadKeyboardButtons() : 0;
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
||||
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;
|
||||
data[0x04] = leftX;
|
||||
data[0x05] = leftY;
|
||||
data[0x06] = rightX;
|
||||
data[0x07] = rightY;
|
||||
data[0x08] = acceptsKeyboardInput && IsKeyDown(0x52) ? (byte)255 : (byte)0;
|
||||
data[0x09] = acceptsKeyboardInput && IsKeyDown(0x46) ? (byte)255 : (byte)0;
|
||||
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();
|
||||
@@ -206,58 +185,4 @@ public static class PadExports
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern short GetAsyncKeyState(int vKey);
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern nint GetForegroundWindow();
|
||||
|
||||
[DllImport("user32.dll")]
|
||||
private static extern uint GetWindowThreadProcessId(nint hWnd, out uint processId);
|
||||
|
||||
private static bool IsKeyDown(int vk) =>
|
||||
(GetAsyncKeyState(vk) & 0x8000) != 0;
|
||||
|
||||
private static bool IsEmulatorWindowFocused()
|
||||
{
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
@@ -256,7 +256,7 @@ public static class PlayGoExports
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -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;
|
||||
}
|
||||
@@ -748,6 +748,13 @@ public static class PlayGoExports
|
||||
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)];
|
||||
|
||||
@@ -60,7 +60,7 @@ public static class SaveDataDialogExports
|
||||
}
|
||||
|
||||
_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.
|
||||
@@ -182,6 +182,19 @@ 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);
|
||||
|
||||
@@ -2,7 +2,6 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
@@ -11,8 +10,6 @@ namespace SharpEmu.Libs.SaveData;
|
||||
public static class SaveDataExports
|
||||
{
|
||||
private const int OrbisSaveDataErrorParameter = unchecked((int)0x809F0000);
|
||||
private const int OrbisSaveDataErrorExists = unchecked((int)0x809F0007);
|
||||
private const int OrbisSaveDataErrorNotFound = unchecked((int)0x809F0008);
|
||||
private const int OrbisSaveDataErrorInternal = unchecked((int)0x809F000B);
|
||||
private const int SaveDataTitleIdSize = 10;
|
||||
private const int SaveDataDirNameSize = 32;
|
||||
@@ -26,9 +23,6 @@ public static class SaveDataExports
|
||||
private const ulong ResultInfosOffset = 0x20;
|
||||
private const uint SortKeyFreeBlocks = 5;
|
||||
private const uint SortOrderDescent = 1;
|
||||
private const uint MountModeCreate = 1u << 2;
|
||||
private const uint MountModeCreate2 = 1u << 5;
|
||||
private const int MountResultSize = 0x40;
|
||||
private static readonly object _stateGate = new();
|
||||
private static string? _titleId;
|
||||
|
||||
@@ -108,8 +102,8 @@ 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 SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
@@ -155,133 +149,14 @@ public static class SaveDataExports
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ZP4e7rlzOUk",
|
||||
ExportName = "sceSaveDataMount3",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceSaveData")]
|
||||
public static int SaveDataMount3(CpuContext ctx)
|
||||
{
|
||||
var mountAddress = ctx[CpuRegister.Rdi];
|
||||
var resultAddress = ctx[CpuRegister.Rsi];
|
||||
if (mountAddress == 0 || resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
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) ||
|
||||
dirNameAddress == 0 ||
|
||||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (userId < 0 || string.IsNullOrWhiteSpace(dirName))
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
var titleId = ResolveConfiguredTitleId();
|
||||
var savePath = Path.Combine(
|
||||
ResolveTitleSaveRoot(userId, titleId),
|
||||
SanitizePathSegment(dirName));
|
||||
var existed = Directory.Exists(savePath);
|
||||
var create = (mountMode & MountModeCreate) != 0;
|
||||
var createIfMissing = (mountMode & MountModeCreate2) != 0;
|
||||
|
||||
if (!existed && !create && !createIfMissing)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorNotFound);
|
||||
}
|
||||
|
||||
if (existed && create)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorExists);
|
||||
}
|
||||
|
||||
if (!existed)
|
||||
{
|
||||
Directory.CreateDirectory(savePath);
|
||||
}
|
||||
|
||||
const string mountPoint = "/savedata0";
|
||||
KernelMemoryCompatExports.RegisterGuestPathMount(mountPoint, savePath);
|
||||
|
||||
Span<byte> result = stackalloc byte[MountResultSize];
|
||||
result.Clear();
|
||||
WriteAscii(result[..16], mountPoint);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(result[0x1C..], createIfMissing && !existed ? 1u : 0u);
|
||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||
{
|
||||
return 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 SetReturn(ctx, 0);
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
return SetReturn(ctx, OrbisSaveDataErrorInternal);
|
||||
}
|
||||
catch (ArgumentException)
|
||||
{
|
||||
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 SetReturn(ctx, OrbisSaveDataErrorParameter);
|
||||
}
|
||||
|
||||
var id = (uint)Interlocked.Increment(ref _nextTransactionResource);
|
||||
|
||||
if (!ctx.TryWriteUInt32(resourceAddress, id))
|
||||
{
|
||||
return SetReturn(ctx, (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 SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
||||
{
|
||||
cond = default;
|
||||
if (!TryReadInt32(ctx, address, out var userId) ||
|
||||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
||||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
||||
!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 +179,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 +305,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);
|
||||
}
|
||||
@@ -518,6 +393,26 @@ public static class SaveDataExports
|
||||
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);
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
@@ -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, )",
|
||||
|
||||
@@ -4,7 +4,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
</Project>
|
||||
|
||||
Reference in New Issue
Block a user