mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-30 14:39:42 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| dfe2b70b38 | |||
| 9b3a73b4bf | |||
| 9ab1e2f21a | |||
| 340eb9e0ab | |||
| 03e3d25e85 | |||
| 2bc466b10e | |||
| 45d0be474b | |||
| 5bb91eff9b | |||
| f4df0ed4bd | |||
| 23691a2bdc | |||
| 5c5ed8c064 | |||
| a4748ce266 | |||
| 5617646c8a | |||
| a4f3d3cd7f | |||
| 92526ecacf |
@@ -37,5 +37,3 @@ packages/
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
Desktop.ini
|
Desktop.ini
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
|
|
||||||
.vs/
|
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright (C) 2026 SharpEmu Emulator Project
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<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="Iced" Version="1.21.0" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.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.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Windowing" 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>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Folder Name="/src/">
|
<Folder Name="/src/">
|
||||||
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
|
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
|
||||||
<Project Path="src/SharpEmu.Core/SharpEmu.Core.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.HLE/SharpEmu.HLE.csproj" />
|
||||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "10.0.103",
|
"version": "10.0.103",
|
||||||
"rollForward": "latestFeature"
|
"rollForward": "disable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-102
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using SharpEmu.Core.Runtime;
|
using SharpEmu.Core.Runtime;
|
||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
using SharpEmu.GUI;
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
using System.Runtime.InteropServices;
|
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_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_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 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)
|
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");
|
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||||
|
|
||||||
|
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||||
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
||||||
{
|
{
|
||||||
return childExitCode;
|
return childExitCode;
|
||||||
@@ -122,58 +101,6 @@ internal static partial class Program
|
|||||||
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
|
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)
|
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
||||||
{
|
{
|
||||||
isMitigatedChild = false;
|
isMitigatedChild = false;
|
||||||
@@ -743,32 +670,4 @@ internal static partial class Program
|
|||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool CloseHandle(nint handle);
|
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,19 +7,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
|
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||||
<ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" />
|
|
||||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- GUI subsystem so starting without arguments opens the frontend with no
|
<OutputType>Exe</OutputType>
|
||||||
console window; CLI mode re-attaches to the parent terminal's console. -->
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<AssemblyName>SharpEmu</AssemblyName>
|
<AssemblyName>SharpEmu</AssemblyName>
|
||||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
|
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
<PublishSingleFile>true</PublishSingleFile>
|
<PublishSingleFile>true</PublishSingleFile>
|
||||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
<!-- <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> -->
|
||||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||||
<ImplicitUsings>enable</ImplicitUsings>
|
<ImplicitUsings>enable</ImplicitUsings>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
@@ -38,10 +35,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
|
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="..\..\LICENSE.txt">
|
<Content Include="..\..\LICENSE.txt">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
@@ -50,17 +43,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</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>
|
</Project>
|
||||||
|
|||||||
@@ -4,108 +4,9 @@
|
|||||||
"net10.0": {
|
"net10.0": {
|
||||||
"Microsoft.NET.ILLink.Tasks": {
|
"Microsoft.NET.ILLink.Tasks": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[10.0.9, )",
|
"requested": "[10.0.3, )",
|
||||||
"resolved": "10.0.9",
|
"resolved": "10.0.3",
|
||||||
"contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg=="
|
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||||
},
|
|
||||||
"Avalonia.Angle.Windows.Natives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.1.25547.20250602",
|
|
||||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
|
||||||
},
|
|
||||||
"Avalonia.BuildServices": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.2",
|
|
||||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
|
||||||
},
|
|
||||||
"Avalonia.FreeDesktop": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18",
|
|
||||||
"Tmds.DBus.Protocol": "0.21.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Avalonia.Native": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Avalonia.Remote.Protocol": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
|
||||||
},
|
|
||||||
"Avalonia.Skia": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18",
|
|
||||||
"HarfBuzzSharp": "8.3.1.1",
|
|
||||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
|
||||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
|
||||||
"SkiaSharp": "2.88.9",
|
|
||||||
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
|
||||||
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Avalonia.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18",
|
|
||||||
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Avalonia.X11": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18",
|
|
||||||
"Avalonia.FreeDesktop": "11.3.18",
|
|
||||||
"Avalonia.Skia": "11.3.18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
|
||||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
|
||||||
},
|
|
||||||
"MicroCom.Runtime": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "0.11.0",
|
|
||||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
|
||||||
},
|
},
|
||||||
"Microsoft.DotNet.PlatformAbstractions": {
|
"Microsoft.DotNet.PlatformAbstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -158,38 +59,6 @@
|
|||||||
"Silk.NET.Windowing.Common": "2.23.0"
|
"Silk.NET.Windowing.Common": "2.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SkiaSharp": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
|
||||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.Linux": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
|
||||||
"dependencies": {
|
|
||||||
"SkiaSharp": "2.88.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.macOS": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
|
||||||
},
|
|
||||||
"Ultz.Native.GLFW": {
|
"Ultz.Native.GLFW": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.4.0",
|
"resolved": "3.4.0",
|
||||||
@@ -204,16 +73,6 @@
|
|||||||
"SharpEmu.Logging": "[1.0.0, )"
|
"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": {
|
"sharpemu.hle": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
},
|
},
|
||||||
@@ -222,7 +81,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SharpEmu.HLE": "[1.0.0, )",
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
"Silk.NET.Vulkan": "[2.23.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.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
"Silk.NET.Windowing": "[2.23.0, )"
|
"Silk.NET.Windowing": "[2.23.0, )"
|
||||||
}
|
}
|
||||||
@@ -230,48 +88,6 @@
|
|||||||
"sharpemu.logging": {
|
"sharpemu.logging": {
|
||||||
"type": "Project"
|
"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": {
|
"Iced": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[1.21.0, )",
|
"requested": "[1.21.0, )",
|
||||||
@@ -287,16 +103,6 @@
|
|||||||
"Silk.NET.Core": "2.23.0"
|
"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": {
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
@@ -316,61 +122,9 @@
|
|||||||
"Silk.NET.Windowing.Common": "2.23.0",
|
"Silk.NET.Windowing.Common": "2.23.0",
|
||||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Tmds.DBus.Protocol": {
|
|
||||||
"type": "CentralTransitive",
|
|
||||||
"requested": "[0.21.3, )",
|
|
||||||
"resolved": "0.21.3",
|
|
||||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net10.0/linux-x64": {
|
"net10.0/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": {
|
"Ultz.Native.GLFW": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.4.0",
|
"resolved": "3.4.0",
|
||||||
@@ -378,52 +132,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net10.0/osx-arm64": {
|
"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": {
|
"Ultz.Native.GLFW": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.4.0",
|
"resolved": "3.4.0",
|
||||||
@@ -431,52 +139,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net10.0/win-x64": {
|
"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": {
|
"Ultz.Native.GLFW": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.4.0",
|
"resolved": "3.4.0",
|
||||||
|
|||||||
@@ -415,8 +415,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
|
|
||||||
const ulong entryParamsSize = 0x20;
|
const ulong entryParamsSize = 0x20;
|
||||||
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
||||||
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
|
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
|
||||||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
|
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
||||||
@@ -456,6 +456,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
return value & ~(alignment - 1);
|
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(
|
private static string BuildEntryFrameDiagnostic(
|
||||||
ulong entryPoint,
|
ulong entryPoint,
|
||||||
CpuContext context,
|
CpuContext context,
|
||||||
|
|||||||
@@ -338,15 +338,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
orbisGen2Result = DispatchBootstrapBridge();
|
orbisGen2Result = DispatchBootstrapBridge();
|
||||||
}
|
}
|
||||||
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
|
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal))
|
||||||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
orbisGen2Result = DispatchKernelDynlibDlsym();
|
orbisGen2Result = DispatchKernelDynlibDlsym();
|
||||||
}
|
}
|
||||||
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
|
|
||||||
}
|
|
||||||
else if (importStubEntry.Export is { } cachedExport &&
|
else if (importStubEntry.Export is { } cachedExport &&
|
||||||
(cachedExport.Target & cpuContext.TargetGeneration) != 0)
|
(cachedExport.Target & cpuContext.TargetGeneration) != 0)
|
||||||
{
|
{
|
||||||
@@ -633,56 +628,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||||
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
||||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
"qvMUCyyaCSI"; // sceKernelAprSubmitCommandBufferAndGetId
|
||||||
"Q2V+iqvjgC0" or // vsnprintf
|
|
||||||
"q1cHNfGycLI" or // scePadRead
|
|
||||||
"xk0AcarP3V4" or // scePadOpen
|
|
||||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
|
||||||
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
|
|
||||||
"K-jXhbt2gn4"; // scePthreadMutexTrylock
|
|
||||||
|
|
||||||
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
||||||
{
|
{
|
||||||
var resultValue = unchecked((int)result);
|
|
||||||
if (resultValue > 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expectedFileProbeMiss =
|
var expectedFileProbeMiss =
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND &&
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND &&
|
||||||
IsExpectedFileProbeNotFoundNid(nid);
|
IsExpectedFileProbeNotFoundNid(nid);
|
||||||
var expectedTimedWaitTimeout =
|
var expectedTimedWaitTimeout =
|
||||||
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
|
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
|
||||||
unchecked((int)result) == 60;
|
unchecked((int)result) == 60;
|
||||||
var expectedEqueueTimeout =
|
|
||||||
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
|
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
|
||||||
var expectedMutexTrylockBusy =
|
var expectedMutexTrylockBusy =
|
||||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
var expectedUserServiceNoEvent =
|
if (!expectedFileProbeMiss && !expectedTimedWaitTimeout && !expectedMutexTrylockBusy)
|
||||||
string.Equals(nid, "yH17Q6NWtVg", StringComparison.Ordinal) &&
|
|
||||||
resultValue == unchecked((int)0x80960007);
|
|
||||||
var expectedPrivacyInvalidParameter =
|
|
||||||
string.Equals(nid, "D-CzAxQL0XI", StringComparison.Ordinal) &&
|
|
||||||
resultValue == unchecked((int)0x80960009);
|
|
||||||
if (!expectedFileProbeMiss &&
|
|
||||||
!expectedTimedWaitTimeout &&
|
|
||||||
!expectedEqueueTimeout &&
|
|
||||||
!expectedMutexTrylockBusy &&
|
|
||||||
!expectedUserServiceNoEvent &&
|
|
||||||
!expectedPrivacyInvalidParameter)
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ShouldLogExpectedImportResults())
|
var key = nid + "\0" + (int)result;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var key = nid + "\0" + resultValue;
|
|
||||||
int count;
|
int count;
|
||||||
lock (_importResultLogSampleGate)
|
lock (_importResultLogSampleGate)
|
||||||
{
|
{
|
||||||
@@ -694,12 +658,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return count <= 8 || count % 10000 == 0;
|
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) =>
|
private static bool IsExpectedFileProbeNotFoundNid(string nid) =>
|
||||||
nid is
|
nid is
|
||||||
"eV9wAD2riIA" or // sceKernelStat
|
"eV9wAD2riIA" or // sceKernelStat
|
||||||
@@ -1222,11 +1180,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
|
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress))
|
||||||
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'");
|
|
||||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -1239,62 +1194,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
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()
|
private OrbisGen2Result DispatchBootstrapBridge()
|
||||||
{
|
{
|
||||||
var cpuContext = ActiveCpuContext;
|
var cpuContext = ActiveCpuContext;
|
||||||
|
|||||||
@@ -11,9 +11,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||||
private readonly object _guestAllocationGate = new();
|
private readonly object _guestAllocationGate = new();
|
||||||
private readonly object _allocationSearchHintGate = new();
|
|
||||||
private readonly List<MemoryRegion> _regions = 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 readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private const ulong PageSize = 0x1000;
|
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 GuestAllocationArenaSize = 0x0100_0000;
|
||||||
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
||||||
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
||||||
private const ulong FullCommitRegionLimit = 4UL << 30;
|
private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
|
||||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
|
||||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||||
|
|
||||||
private const uint MEM_COMMIT = 0x1000;
|
private const uint MEM_COMMIT = 0x1000;
|
||||||
@@ -38,7 +35,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private ulong _guestAllocationArenaBase;
|
private ulong _guestAllocationArenaBase;
|
||||||
private ulong _guestAllocationOffset;
|
private ulong _guestAllocationOffset;
|
||||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||||
@@ -100,7 +96,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allocationKind = executable ? "executable memory" : "data memory";
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -114,9 +110,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
||||||
var reservedOnly = false;
|
var reservedOnly = false;
|
||||||
var preferReserveOnly = !executable &&
|
var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold;
|
||||||
alignedSize >= LargeDataReserveThreshold &&
|
|
||||||
alignedSize > FullCommitRegionLimit;
|
|
||||||
|
|
||||||
void* result = null;
|
void* result = null;
|
||||||
if (preferReserveOnly)
|
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)");
|
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);
|
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
@@ -199,12 +193,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
lazyPrimeState = committedBytes == primeBytes
|
lazyPrimeState = committedBytes == primeBytes
|
||||||
? $"ok:{committedBytes:X}"
|
? $"ok:{committedBytes:X}"
|
||||||
: $"partial:{committedBytes:X}/{primeBytes: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
|
else
|
||||||
{
|
{
|
||||||
lazyPrimeState = $"fail:{primeBytes:X}";
|
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
|
else
|
||||||
@@ -233,7 +227,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var allocationKind = reservedOnly
|
var allocationKind = reservedOnly
|
||||||
? "reserved data memory (lazy commit)"
|
? "reserved data memory (lazy commit)"
|
||||||
: (executable ? "executable memory" : "data memory");
|
: (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;
|
return actualAddress;
|
||||||
}
|
}
|
||||||
@@ -253,8 +247,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var alignedSize = AlignUp(size, PageSize);
|
var alignedSize = AlignUp(size, PageSize);
|
||||||
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
|
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
|
||||||
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
var cursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
|
||||||
|
|
||||||
for (var attempt = 0; attempt < 0x10000; attempt++)
|
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);
|
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
||||||
if (actualAddress == cursor)
|
if (actualAddress == cursor)
|
||||||
{
|
{
|
||||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,10 +334,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
_regions.Clear();
|
_regions.Clear();
|
||||||
_pageProtections.Clear();
|
_pageProtections.Clear();
|
||||||
lock (_allocationSearchHintGate)
|
|
||||||
{
|
|
||||||
_allocationSearchHints.Clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -402,7 +390,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
ApplySegmentProtection(mapStart, mapEnd, protection);
|
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
|
finally
|
||||||
{
|
{
|
||||||
@@ -805,16 +793,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
var regionEnd = region.VirtualAddress + region.Size;
|
var regionEnd = region.VirtualAddress + region.Size;
|
||||||
if (region.VirtualAddress >= end)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (regionEnd <= address)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (address < regionEnd && region.VirtualAddress < end)
|
if (address < regionEnd && region.VirtualAddress < end)
|
||||||
{
|
{
|
||||||
overlapEnd = Math.Max(overlapEnd, regionEnd);
|
overlapEnd = Math.Max(overlapEnd, regionEnd);
|
||||||
@@ -829,37 +807,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return overlapEnd != 0;
|
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)
|
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
|
||||||
{
|
{
|
||||||
offset = 0;
|
offset = 0;
|
||||||
@@ -1028,29 +975,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return checked((value + mask) & ~mask);
|
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()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
|
|||||||
@@ -543,7 +543,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
Path.Combine(ebootDirectory, "sce_module"),
|
Path.Combine(ebootDirectory, "sce_module"),
|
||||||
Path.Combine(ebootDirectory, "sce_modules"),
|
Path.Combine(ebootDirectory, "sce_modules"),
|
||||||
Path.Combine(ebootDirectory, "Media", "Modules"),
|
|
||||||
}
|
}
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.Where(Directory.Exists)
|
.Where(Directory.Exists)
|
||||||
@@ -555,9 +554,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allModulePaths = moduleDirectories
|
var allModulePaths = moduleDirectories
|
||||||
.SelectMany(directory => Directory
|
.SelectMany(Directory.EnumerateFiles)
|
||||||
.EnumerateFiles(directory)
|
|
||||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
|
||||||
.Where(path =>
|
.Where(path =>
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
@@ -565,6 +562,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
||||||
})
|
})
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var modulePaths = allModulePaths
|
var modulePaths = allModulePaths
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright (C) 2026 SharpEmu Emulator Project
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
-->
|
-->
|
||||||
@@ -14,10 +14,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<PackageReference Include="Iced" />
|
<PackageReference Include="Iced" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -72,7 +72,6 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SharpEmu.HLE": "[1.0.0, )",
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
"Silk.NET.Vulkan": "[2.23.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.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
"Silk.NET.Windowing": "[2.23.0, )"
|
"Silk.NET.Windowing": "[2.23.0, )"
|
||||||
}
|
}
|
||||||
@@ -89,16 +88,6 @@
|
|||||||
"Silk.NET.Core": "2.23.0"
|
"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": {
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.23.0, )",
|
"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
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
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[] _registers = new ulong[16];
|
||||||
private readonly ulong[] _xmmRegisters = new ulong[32];
|
private readonly ulong[] _xmmRegisters = new ulong[32];
|
||||||
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
|
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
|
||||||
private bool _raxWritten;
|
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; }
|
public ulong Rip { get; set; }
|
||||||
|
|
||||||
@@ -125,39 +130,6 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
SetYmmUpper(registerIndex, highLow, highHigh);
|
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)
|
public bool TryReadUInt64(ulong address, out ulong value)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
@@ -178,33 +150,6 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
return Memory.TryWrite(address, buffer);
|
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)
|
public bool PushUInt64(ulong value)
|
||||||
{
|
{
|
||||||
var rsp = this[CpuRegister.Rsp];
|
var rsp = this[CpuRegister.Rsp];
|
||||||
|
|||||||
@@ -8,10 +8,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
+352
-432
File diff suppressed because it is too large
Load Diff
@@ -18,9 +18,9 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
out Gen5ShaderMetadata metadata)
|
out Gen5ShaderMetadata metadata)
|
||||||
{
|
{
|
||||||
metadata = default!;
|
metadata = default!;
|
||||||
if (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
if (!TryReadUInt64(ctx, shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
||||||
userDataAddress == 0 ||
|
userDataAddress == 0 ||
|
||||||
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
|
!TryReadUInt64(ctx, userDataAddress, out var directResourceOffsetsAddress))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -28,7 +28,8 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
var resourceOffsets = new ulong[ResourceClassCount];
|
var resourceOffsets = new ulong[ResourceClassCount];
|
||||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt64(
|
if (!TryReadUInt64(
|
||||||
|
ctx,
|
||||||
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
|
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
|
||||||
out resourceOffsets[resourceClass]))
|
out resourceOffsets[resourceClass]))
|
||||||
{
|
{
|
||||||
@@ -137,4 +138,17 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
@@ -1404,7 +1405,8 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(
|
if (!TryReadUInt32(
|
||||||
|
ctx,
|
||||||
address + (ulong)(index * sizeof(uint)),
|
address + (ulong)(index * sizeof(uint)),
|
||||||
out var value) &&
|
out var value) &&
|
||||||
!TryReadUserDataScalarLoad(
|
!TryReadUserDataScalarLoad(
|
||||||
@@ -1725,4 +1727,16 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
|
opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
|
||||||
opcode.StartsWith("ImageGather", StringComparison.Ordinal);
|
opcode.StartsWith("ImageGather", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -294,7 +294,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
var instructionCount = 0;
|
var instructionCount = 0;
|
||||||
for (uint pc = 0; instructionCount < MaxInstructions;)
|
for (uint pc = 0; instructionCount < MaxInstructions;)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(address + pc, out var word))
|
if (!TryReadUInt32(ctx, address + pc, out var word))
|
||||||
{
|
{
|
||||||
error = $"read-failed pc=0x{pc:X}";
|
error = $"read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -317,7 +317,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
words[0] = word;
|
words[0] = word;
|
||||||
for (uint wordIndex = 1; wordIndex < sizeDwords; wordIndex++)
|
for (uint wordIndex = 1; wordIndex < sizeDwords; wordIndex++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(address + pc + wordIndex * sizeof(uint), out words[wordIndex]))
|
if (!TryReadUInt32(ctx, address + pc + wordIndex * sizeof(uint), out words[wordIndex]))
|
||||||
{
|
{
|
||||||
error = $"read-failed pc=0x{pc + wordIndex * sizeof(uint):X}";
|
error = $"read-failed pc=0x{pc + wordIndex * sizeof(uint):X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -397,7 +397,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
case 0x34:
|
case 0x34:
|
||||||
case 0x35:
|
case 0x35:
|
||||||
encoding = Gen5ShaderEncoding.Vop3;
|
encoding = Gen5ShaderEncoding.Vop3;
|
||||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var vop3Extra))
|
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var vop3Extra))
|
||||||
{
|
{
|
||||||
error = $"vop3-extra-read-failed pc=0x{pc:X}";
|
error = $"vop3-extra-read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -418,7 +418,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
return DecodeFlat(word, out name, out sizeDwords, out error);
|
return DecodeFlat(word, out name, out sizeDwords, out error);
|
||||||
case 0x38:
|
case 0x38:
|
||||||
encoding = Gen5ShaderEncoding.Mubuf;
|
encoding = Gen5ShaderEncoding.Mubuf;
|
||||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var mubufExtra))
|
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mubufExtra))
|
||||||
{
|
{
|
||||||
error = $"mubuf-extra-read-failed pc=0x{pc:X}";
|
error = $"mubuf-extra-read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -427,7 +427,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
return DecodeMubuf(word, mubufExtra, out name, out sizeDwords, out error);
|
return DecodeMubuf(word, mubufExtra, out name, out sizeDwords, out error);
|
||||||
case 0x3A:
|
case 0x3A:
|
||||||
encoding = Gen5ShaderEncoding.Mtbuf;
|
encoding = Gen5ShaderEncoding.Mtbuf;
|
||||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var mtbufExtra))
|
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mtbufExtra))
|
||||||
{
|
{
|
||||||
error = $"mtbuf-extra-read-failed pc=0x{pc:X}";
|
error = $"mtbuf-extra-read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -1741,6 +1741,19 @@ internal static class Gen5ShaderTranslator
|
|||||||
counts[key] = count + 1;
|
counts[key] = count + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly record struct ShaderDecodeInfo(
|
private readonly record struct ShaderDecodeInfo(
|
||||||
int InstructionCount,
|
int InstructionCount,
|
||||||
Dictionary<string, int> Counts,
|
Dictionary<string, int> Counts,
|
||||||
|
|||||||
@@ -467,6 +467,9 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
_imageBindingByPc.TryAdd(binding.Pc, index);
|
_imageBindingByPc.TryAdd(binding.Pc, index);
|
||||||
var isStorage =
|
var isStorage =
|
||||||
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
|
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
|
||||||
|
var isDepth =
|
||||||
|
binding.Opcode.Contains("SampleC", StringComparison.Ordinal) ||
|
||||||
|
binding.Opcode.Contains("Gather4C", StringComparison.Ordinal);
|
||||||
var (format, componentKind) =
|
var (format, componentKind) =
|
||||||
DecodeImageFormat(binding.ResourceDescriptor);
|
DecodeImageFormat(binding.ResourceDescriptor);
|
||||||
var componentType = componentKind switch
|
var componentType = componentKind switch
|
||||||
@@ -491,7 +494,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var imageType = _module.TypeImage(
|
var imageType = _module.TypeImage(
|
||||||
componentType,
|
componentType,
|
||||||
SpirvImageDim.Dim2D,
|
SpirvImageDim.Dim2D,
|
||||||
depth: false,
|
depth: isDepth,
|
||||||
arrayed: false,
|
arrayed: false,
|
||||||
multisampled: false,
|
multisampled: false,
|
||||||
sampled: isStorage ? 2u : 1u,
|
sampled: isStorage ? 2u : 1u,
|
||||||
@@ -1658,14 +1661,18 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
||||||
var imageOperands =
|
var imageOperands =
|
||||||
(explicitLod ? 2u : 0u) | (hasOffset ? 0x10u : 0u);
|
(explicitLod ? 2u : 0u) | (hasOffset ? 0x10u : 0u);
|
||||||
var reference = hasCompare
|
|
||||||
? Bitcast(_floatType, LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))
|
|
||||||
: 0u;
|
|
||||||
var operands = new List<uint>
|
var operands = new List<uint>
|
||||||
{
|
{
|
||||||
imageObject,
|
imageObject,
|
||||||
coordinates,
|
coordinates,
|
||||||
};
|
};
|
||||||
|
if (hasCompare)
|
||||||
|
{
|
||||||
|
operands.Add(
|
||||||
|
Bitcast(
|
||||||
|
_floatType,
|
||||||
|
LoadV(image.GetAddressRegister(hasOffset ? 1 : 0))));
|
||||||
|
}
|
||||||
|
|
||||||
if (imageOperands != 0)
|
if (imageOperands != 0)
|
||||||
{
|
{
|
||||||
@@ -1682,14 +1689,29 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
sampled = _module.AddInstruction(
|
sampled = _module.AddInstruction(
|
||||||
explicitLod
|
hasCompare
|
||||||
? SpirvOp.ImageSampleExplicitLod
|
? explicitLod
|
||||||
: SpirvOp.ImageSampleImplicitLod,
|
? SpirvOp.ImageSampleDrefExplicitLod
|
||||||
resource.VectorType,
|
: SpirvOp.ImageSampleDrefImplicitLod
|
||||||
|
: explicitLod
|
||||||
|
? SpirvOp.ImageSampleExplicitLod
|
||||||
|
: SpirvOp.ImageSampleImplicitLod,
|
||||||
|
hasCompare ? resource.ComponentType : resource.VectorType,
|
||||||
[.. operands]);
|
[.. operands]);
|
||||||
if (hasCompare)
|
if (hasCompare)
|
||||||
{
|
{
|
||||||
sampled = EmitManualDepthCompare(resource, sampled, reference);
|
var scalar = sampled;
|
||||||
|
sampled = _module.AddInstruction(
|
||||||
|
SpirvOp.CompositeConstruct,
|
||||||
|
resource.VectorType,
|
||||||
|
scalar,
|
||||||
|
scalar,
|
||||||
|
scalar,
|
||||||
|
resource.ComponentKind == ImageComponentKind.Float
|
||||||
|
? Float(1)
|
||||||
|
: resource.ComponentKind == ImageComponentKind.Uint
|
||||||
|
? UInt(1)
|
||||||
|
: _module.Constant(_intType, 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (instruction.Opcode.StartsWith(
|
else if (instruction.Opcode.StartsWith(
|
||||||
@@ -1703,9 +1725,6 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var start = (hasOffset ? 1 : 0) + (hasCompare ? 1 : 0);
|
var start = (hasOffset ? 1 : 0) + (hasCompare ? 1 : 0);
|
||||||
var coordinates = BuildFloatCoordinates(image, start);
|
var coordinates = BuildFloatCoordinates(image, start);
|
||||||
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
||||||
var reference = hasCompare
|
|
||||||
? Bitcast(_floatType, LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))
|
|
||||||
: 0u;
|
|
||||||
var operands = new List<uint>
|
var operands = new List<uint>
|
||||||
{
|
{
|
||||||
imageObject,
|
imageObject,
|
||||||
@@ -1713,7 +1732,10 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
};
|
};
|
||||||
if (hasCompare)
|
if (hasCompare)
|
||||||
{
|
{
|
||||||
operands.Add(UInt(0));
|
operands.Add(
|
||||||
|
Bitcast(
|
||||||
|
_floatType,
|
||||||
|
LoadV(image.GetAddressRegister(hasOffset ? 1 : 0))));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1734,28 +1756,11 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
sampled = _module.AddInstruction(
|
sampled = _module.AddInstruction(
|
||||||
SpirvOp.ImageGather,
|
hasCompare
|
||||||
|
? SpirvOp.ImageDrefGather
|
||||||
|
: SpirvOp.ImageGather,
|
||||||
resource.VectorType,
|
resource.VectorType,
|
||||||
[.. operands]);
|
[.. operands]);
|
||||||
if (hasCompare)
|
|
||||||
{
|
|
||||||
var compared = new uint[4];
|
|
||||||
for (var component = 0u; component < 4; component++)
|
|
||||||
{
|
|
||||||
var texel = _module.AddInstruction(
|
|
||||||
SpirvOp.CompositeExtract,
|
|
||||||
resource.ComponentType,
|
|
||||||
sampled,
|
|
||||||
component);
|
|
||||||
compared[component] = EmitDepthCompareScalar(resource, texel, reference);
|
|
||||||
}
|
|
||||||
|
|
||||||
sampled = _module.AddInstruction(
|
|
||||||
SpirvOp.CompositeConstruct,
|
|
||||||
resource.VectorType,
|
|
||||||
compared);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeAllComponents = true;
|
writeAllComponents = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1789,67 +1794,6 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private uint EmitDepthCompareScalar(
|
|
||||||
SpirvImageResource resource,
|
|
||||||
uint texel,
|
|
||||||
uint reference)
|
|
||||||
{
|
|
||||||
var texelAsFloat = resource.ComponentKind switch
|
|
||||||
{
|
|
||||||
ImageComponentKind.Uint => _module.AddInstruction(
|
|
||||||
SpirvOp.ConvertUToF, _floatType, texel),
|
|
||||||
ImageComponentKind.Sint => _module.AddInstruction(
|
|
||||||
SpirvOp.ConvertSToF, _floatType, texel),
|
|
||||||
_ => texel,
|
|
||||||
};
|
|
||||||
var passes = _module.AddInstruction(
|
|
||||||
SpirvOp.FOrdLessThanEqual,
|
|
||||||
_boolType,
|
|
||||||
reference,
|
|
||||||
texelAsFloat);
|
|
||||||
return _module.AddInstruction(
|
|
||||||
SpirvOp.Select,
|
|
||||||
resource.ComponentType,
|
|
||||||
passes,
|
|
||||||
resource.ComponentKind switch
|
|
||||||
{
|
|
||||||
ImageComponentKind.Uint => UInt(1),
|
|
||||||
ImageComponentKind.Sint => _module.Constant(_intType, 1),
|
|
||||||
_ => Float(1),
|
|
||||||
},
|
|
||||||
resource.ComponentKind switch
|
|
||||||
{
|
|
||||||
ImageComponentKind.Uint => UInt(0),
|
|
||||||
ImageComponentKind.Sint => _module.Constant(_intType, 0),
|
|
||||||
_ => Float(0),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private uint EmitManualDepthCompare(
|
|
||||||
SpirvImageResource resource,
|
|
||||||
uint sampledVector,
|
|
||||||
uint reference)
|
|
||||||
{
|
|
||||||
var texel = _module.AddInstruction(
|
|
||||||
SpirvOp.CompositeExtract,
|
|
||||||
resource.ComponentType,
|
|
||||||
sampledVector,
|
|
||||||
0u);
|
|
||||||
var scalar = EmitDepthCompareScalar(resource, texel, reference);
|
|
||||||
return _module.AddInstruction(
|
|
||||||
SpirvOp.CompositeConstruct,
|
|
||||||
resource.VectorType,
|
|
||||||
scalar,
|
|
||||||
scalar,
|
|
||||||
scalar,
|
|
||||||
resource.ComponentKind switch
|
|
||||||
{
|
|
||||||
ImageComponentKind.Uint => UInt(1),
|
|
||||||
ImageComponentKind.Sint => _module.Constant(_intType, 1),
|
|
||||||
_ => Float(1),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private uint BuildFloatCoordinates(Gen5ImageControl image, int start)
|
private uint BuildFloatCoordinates(Gen5ImageControl image, int start)
|
||||||
{
|
{
|
||||||
var x = Bitcast(
|
var x = Bitcast(
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ public static class AmprExports
|
|||||||
private const uint KernelEventQueueRecordType = 2;
|
private const uint KernelEventQueueRecordType = 2;
|
||||||
private const uint WriteAddressRecordType = 3;
|
private const uint WriteAddressRecordType = 3;
|
||||||
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
||||||
private static readonly ConcurrentDictionary<string, Lazy<CachedHostFile>> _hostFileCache = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private static readonly bool _traceAmpr =
|
private static readonly bool _traceAmpr =
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||||
private static readonly bool _traceAmprReads =
|
private static readonly bool _traceAmprReads =
|
||||||
@@ -38,23 +37,6 @@ public static class AmprExports
|
|||||||
public ulong WriteOffset;
|
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(
|
[SysAbiExport(
|
||||||
Nid = "8aI7R7WaOlc",
|
Nid = "8aI7R7WaOlc",
|
||||||
ExportName = "sceAmprCommandBufferConstructor",
|
ExportName = "sceAmprCommandBufferConstructor",
|
||||||
@@ -267,7 +249,7 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
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);
|
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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
@@ -453,7 +435,7 @@ public static class AmprExports
|
|||||||
var offset = 0UL;
|
var offset = 0UL;
|
||||||
while (offset < writeOffset)
|
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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -652,43 +634,24 @@ public static class AmprExports
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!TryGetCachedHostFile(hostPath, out var cachedFile, out var openResult))
|
using var stream = new FileStream(
|
||||||
{
|
hostPath,
|
||||||
return openResult;
|
FileMode.Open,
|
||||||
}
|
FileAccess.Read,
|
||||||
|
FileShare.ReadWrite | FileShare.Delete,
|
||||||
long fileLength;
|
ChunkSize,
|
||||||
lock (cachedFile.Gate)
|
FileOptions.SequentialScan);
|
||||||
{
|
if (fileOffset >= (ulong)stream.Length)
|
||||||
fileLength = cachedFile.Stream.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileOffset >= (ulong)fileLength)
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stream.Position = unchecked((long)fileOffset);
|
||||||
|
|
||||||
while (bytesRead < size)
|
while (bytesRead < size)
|
||||||
{
|
{
|
||||||
if (bytesRead > ulong.MaxValue - fileOffset)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var absoluteOffset = fileOffset + bytesRead;
|
|
||||||
if (absoluteOffset > long.MaxValue)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
||||||
int read;
|
var read = stream.Read(buffer, 0, request);
|
||||||
lock (cachedFile.Gate)
|
|
||||||
{
|
|
||||||
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
|
|
||||||
read = cachedFile.Stream.Read(buffer, 0, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (read <= 0)
|
if (read <= 0)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -718,44 +681,6 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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(
|
private static bool AppendReadFileRecord(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong commandBuffer,
|
ulong commandBuffer,
|
||||||
@@ -883,6 +808,19 @@ public static class AmprExports
|
|||||||
return true;
|
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)
|
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
||||||
{
|
{
|
||||||
if (!_traceAmpr)
|
if (!_traceAmpr)
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ public static class AudioOut2Exports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
return TryWriteUInt64(ctx, outContextAddress, handle)
|
||||||
? SetReturn(ctx, 0)
|
? SetReturn(ctx, 0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: 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 portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
||||||
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
||||||
return ctx.TryWriteUInt64(outPortAddress, handle)
|
return TryWriteUInt64(ctx, outPortAddress, handle)
|
||||||
? SetReturn(ctx, 0)
|
? SetReturn(ctx, 0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
@@ -211,11 +211,18 @@ public static class AudioOut2Exports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||||
return ctx.TryWriteUInt64(outUserAddress, handle)
|
return TryWriteUInt64(ctx, outUserAddress, handle)
|
||||||
? SetReturn(ctx, 0)
|
? SetReturn(ctx, 0)
|
||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: 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)
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)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,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 SetReturn(ctx, FiberErrorAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
|
||||||
? SetReturn(ctx, 0)
|
? SetReturn(ctx, 0)
|
||||||
: SetReturn(ctx, FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
@@ -130,7 +130,7 @@ public static class FiberExports
|
|||||||
return SetReturn(ctx, error);
|
return SetReturn(ctx, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
@@ -143,7 +143,7 @@ public static class FiberExports
|
|||||||
_continuations.TryRemove(fiber, out _);
|
_continuations.TryRemove(fiber, out _);
|
||||||
_returnTargets.TryRemove(fiber, out _);
|
_returnTargets.TryRemove(fiber, out _);
|
||||||
_stackRanges.TryRemove(fiber, out _);
|
_stackRanges.TryRemove(fiber, out _);
|
||||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
|
||||||
return SetReturn(ctx, 0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -240,7 +240,7 @@ public static class FiberExports
|
|||||||
|
|
||||||
var returnArgument = ctx[CpuRegister.Rdi];
|
var returnArgument = ctx[CpuRegister.Rdi];
|
||||||
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
||||||
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
|
if (argOnRunAddress != 0 && !TryWriteUInt64(ctx, argOnRunAddress, 0))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
@@ -264,7 +264,7 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
||||||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
||||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun))
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
return SetReturn(ctx, FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
@@ -284,7 +284,7 @@ public static class FiberExports
|
|||||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
@@ -318,7 +318,7 @@ public static class FiberExports
|
|||||||
return SetReturn(ctx, FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
return TryWriteUInt64(ctx, outAddress, fiberAddress)
|
||||||
? SetReturn(ctx, 0)
|
? SetReturn(ctx, 0)
|
||||||
: SetReturn(ctx, FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
@@ -342,7 +342,7 @@ public static class FiberExports
|
|||||||
return SetReturn(ctx, error);
|
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);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
@@ -352,12 +352,12 @@ public static class FiberExports
|
|||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
|
||||||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
|
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
|
||||||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
|
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
|
||||||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
|
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
|
||||||
!TryWriteName(ctx, info + 40, fields.Name) ||
|
!TryWriteName(ctx, info + 40, fields.Name) ||
|
||||||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
@@ -384,7 +384,7 @@ public static class FiberExports
|
|||||||
return SetReturn(ctx, FiberErrorNull);
|
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);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
@@ -441,7 +441,7 @@ public static class FiberExports
|
|||||||
return SetReturn(ctx, FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
return TryWriteUInt64(ctx, outAddress, ctx[CpuRegister.Rbp])
|
||||||
? SetReturn(ctx, 0)
|
? SetReturn(ctx, 0)
|
||||||
: SetReturn(ctx, FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
@@ -482,12 +482,12 @@ public static class FiberExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (optParam != 0 &&
|
if (optParam != 0 &&
|
||||||
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
|
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
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);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
@@ -497,25 +497,25 @@ public static class FiberExports
|
|||||||
flags |= FiberFlagContextSizeCheck;
|
flags |= FiberFlagContextSizeCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
|
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
|
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
|
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
|
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextAddress != 0)
|
if (contextAddress != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
if (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
@@ -616,9 +616,9 @@ public static class FiberExports
|
|||||||
|
|
||||||
if (previousFiber != 0)
|
if (previousFiber != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
|
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
|
||||||
previousState != FiberStateRun ||
|
previousState != FiberStateRun ||
|
||||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
@@ -631,12 +631,12 @@ public static class FiberExports
|
|||||||
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
|
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
|
||||||
{
|
{
|
||||||
if (previousFiber != 0)
|
if (previousFiber != 0)
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(previousFiber, out _);
|
_continuations.TryRemove(previousFiber, out _);
|
||||||
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
|
||||||
}
|
}
|
||||||
_returnTargets.TryRemove(fiber, out _);
|
_returnTargets.TryRemove(fiber, out _);
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
@@ -673,7 +673,7 @@ public static class FiberExports
|
|||||||
|
|
||||||
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
||||||
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
||||||
if (!ctx.TryWriteUInt64(entryRsp, 0))
|
if (!TryWriteUInt64(ctx, entryRsp, 0))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -708,7 +708,7 @@ public static class FiberExports
|
|||||||
FiberContinuation continuation,
|
FiberContinuation continuation,
|
||||||
ulong argument) =>
|
ulong argument) =>
|
||||||
continuation.ArgOnRunAddress == 0 ||
|
continuation.ArgOnRunAddress == 0 ||
|
||||||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
|
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
|
||||||
|
|
||||||
private static GuestCpuContinuation CaptureContinuation(
|
private static GuestCpuContinuation CaptureContinuation(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
@@ -779,11 +779,11 @@ public static class FiberExports
|
|||||||
return FiberErrorInvalid;
|
return FiberErrorInvalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||||
{
|
{
|
||||||
return FiberErrorInvalid;
|
return FiberErrorInvalid;
|
||||||
}
|
}
|
||||||
@@ -839,8 +839,8 @@ public static class FiberExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
|
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
|
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||||
magicStart != FiberSignature0 ||
|
magicStart != FiberSignature0 ||
|
||||||
magicEnd != FiberSignature1)
|
magicEnd != FiberSignature1)
|
||||||
{
|
{
|
||||||
@@ -855,12 +855,12 @@ public static class FiberExports
|
|||||||
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
||||||
{
|
{
|
||||||
fields = default;
|
fields = default;
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
|
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
|
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
|
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
|
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||||
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
|
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
|
||||||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -898,6 +898,46 @@ public static class FiberExports
|
|||||||
return 0;
|
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)
|
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
||||||
@@ -925,6 +965,31 @@ public static class FiberExports
|
|||||||
return true;
|
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)
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Ampr;
|
using SharpEmu.Libs.Ampr;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ public static class KernelAprCompatExports
|
|||||||
return completionResult;
|
return completionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -140,7 +141,7 @@ public static class KernelAprCompatExports
|
|||||||
return completionResult;
|
return completionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -177,6 +178,13 @@ public static class KernelAprCompatExports
|
|||||||
return tag is 0x0C or 0x10;
|
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(
|
private static void TraceApr(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
string operation,
|
string operation,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
@@ -54,7 +55,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
@@ -218,7 +219,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
uint timeoutUsec = 0;
|
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);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
@@ -233,7 +234,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
@@ -339,7 +340,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
if (waiterCountAddress != 0 &&
|
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);
|
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) =>
|
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
||||||
address == 0 || ctx.TryWriteUInt64(address, 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)
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
{
|
{
|
||||||
var value = (int)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)
|
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}");
|
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)
|
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}");
|
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)
|
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}");
|
builder.Append($" {name}=0x{value:X16}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,24 +276,8 @@ public static class KernelEventQueueCompatExports
|
|||||||
var outCountAddress = ctx[CpuRegister.Rcx];
|
var outCountAddress = ctx[CpuRegister.Rcx];
|
||||||
var timeoutAddress = ctx[CpuRegister.R8];
|
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);
|
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 (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -316,41 +300,6 @@ public static class KernelEventQueueCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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);
|
TraceEventQueue(ctx, "wait", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -381,7 +330,6 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
queue.AddLast(queuedEvent);
|
queue.AddLast(queuedEvent);
|
||||||
queued = true;
|
queued = true;
|
||||||
Monitor.PulseAll(_eventQueueGate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queued)
|
if (queued)
|
||||||
@@ -541,14 +489,12 @@ public static class KernelEventQueueCompatExports
|
|||||||
ulong outCountAddress)
|
ulong outCountAddress)
|
||||||
{
|
{
|
||||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
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 (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
return deliveredCount > 0
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasPendingEvents(ulong handle)
|
private static bool HasPendingEvents(ulong handle)
|
||||||
@@ -654,8 +600,16 @@ public static class KernelEventQueueCompatExports
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
|
var returnRip = 0UL;
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||||
Console.Error.WriteLine(
|
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}");
|
$"[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")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
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(
|
[SysAbiExport(
|
||||||
Nid = "hcuQgD53UxM",
|
Nid = "hcuQgD53UxM",
|
||||||
ExportName = "printf",
|
ExportName = "printf",
|
||||||
|
|||||||
@@ -5,9 +5,11 @@ using SharpEmu.HLE;
|
|||||||
using SharpEmu.Libs.Ampr;
|
using SharpEmu.Libs.Ampr;
|
||||||
using System.Buffers;
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
|
using System.Linq;
|
||||||
using System.Globalization;
|
using System.Globalization;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -106,7 +108,6 @@ public static class KernelMemoryCompatExports
|
|||||||
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
private static readonly Dictionary<string, string> _guestMounts = new(StringComparer.OrdinalIgnoreCase);
|
||||||
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
private static readonly HashSet<string> _tracedStatResults = new(StringComparer.Ordinal);
|
||||||
private static readonly HashSet<string> _negativeStatCache = new(StringComparer.OrdinalIgnoreCase);
|
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 long _nextFileDescriptor = 2;
|
||||||
private static ulong _nextPhysicalAddress;
|
private static ulong _nextPhysicalAddress;
|
||||||
private static ulong _nextVirtualAddress;
|
private static ulong _nextVirtualAddress;
|
||||||
@@ -1341,7 +1342,6 @@ public static class KernelMemoryCompatExports
|
|||||||
if (IsMutatingOpen(flags))
|
if (IsMutatingOpen(flags))
|
||||||
{
|
{
|
||||||
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
||||||
InvalidateAprFileSizeCache(hostPath);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}");
|
LogOpenTrace($"_open file path='{guestPath}' host='{hostPath}' flags=0x{flags:X8} fd={fd}");
|
||||||
@@ -1550,7 +1550,6 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
File.Delete(hostPath);
|
File.Delete(hostPath);
|
||||||
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
InvalidateNegativeStatCacheForPathAndAncestors(guestPath);
|
||||||
InvalidateAprFileSizeCache(hostPath);
|
|
||||||
AddNegativeStatCacheForGuestPath(guestPath);
|
AddNegativeStatCacheForGuestPath(guestPath);
|
||||||
LogOpenTrace($"unlink path='{guestPath}' host='{hostPath}'");
|
LogOpenTrace($"unlink path='{guestPath}' host='{hostPath}'");
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2529,17 +2528,6 @@ public static class KernelMemoryCompatExports
|
|||||||
if (fixedMapping && requestedAddress != 0)
|
if (fixedMapping && requestedAddress != 0)
|
||||||
{
|
{
|
||||||
mappedAddress = requestedAddress;
|
mappedAddress = requestedAddress;
|
||||||
reserved = IsGuestRangeBacked(ctx, requestedAddress, length);
|
|
||||||
if (!reserved)
|
|
||||||
{
|
|
||||||
TryReserveExactGuestVirtualRange(ctx, requestedAddress, length, protection);
|
|
||||||
reserved = IsGuestRangeBacked(ctx, requestedAddress, length);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!reserved)
|
|
||||||
{
|
|
||||||
mappedAddress = 0;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -2550,7 +2538,7 @@ public static class KernelMemoryCompatExports
|
|||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] map_direct reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} reserved={reserved} mapped=0x{mappedAddress:X16}");
|
$"[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)
|
if (mappedAddress == 0)
|
||||||
{
|
{
|
||||||
@@ -2608,11 +2596,8 @@ public static class KernelMemoryCompatExports
|
|||||||
var length = ctx[CpuRegister.Rsi];
|
var length = ctx[CpuRegister.Rsi];
|
||||||
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
|
var protection = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
var flags = ctx[CpuRegister.Rcx];
|
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)
|
if (inOutAddressPointer == 0 || length == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
@@ -2634,25 +2619,16 @@ public static class KernelMemoryCompatExports
|
|||||||
if (fixedMapping && requestedAddress != 0)
|
if (fixedMapping && requestedAddress != 0)
|
||||||
{
|
{
|
||||||
mappedAddress = requestedAddress;
|
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))
|
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)
|
if (mappedAddress == 0)
|
||||||
{
|
{
|
||||||
@@ -3997,48 +3973,118 @@ public static class KernelMemoryCompatExports
|
|||||||
ulong alignment,
|
ulong alignment,
|
||||||
out ulong mappedAddress)
|
out ulong mappedAddress)
|
||||||
{
|
{
|
||||||
var executable = (protection & OrbisProtCpuExec) != 0;
|
mappedAddress = 0;
|
||||||
return KernelVirtualRangeAllocator.TryReserve(
|
if (length == 0)
|
||||||
ctx,
|
|
||||||
desiredAddress,
|
|
||||||
length,
|
|
||||||
executable,
|
|
||||||
alignment,
|
|
||||||
allowSearch: true,
|
|
||||||
allowAllocateAtAlternative: false,
|
|
||||||
"reserve range",
|
|
||||||
out mappedAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryReserveExactGuestVirtualRange(
|
|
||||||
CpuContext ctx,
|
|
||||||
ulong desiredAddress,
|
|
||||||
ulong length,
|
|
||||||
int protection)
|
|
||||||
{
|
|
||||||
var executable = (protection & OrbisProtCpuExec) != 0;
|
|
||||||
return KernelVirtualRangeAllocator.TryReserve(
|
|
||||||
ctx,
|
|
||||||
desiredAddress,
|
|
||||||
length,
|
|
||||||
executable,
|
|
||||||
alignment: 0,
|
|
||||||
allowSearch: false,
|
|
||||||
allowAllocateAtAlternative: false,
|
|
||||||
"reserve fixed range",
|
|
||||||
out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool IsGuestRangeBacked(CpuContext ctx, ulong address, ulong length)
|
|
||||||
{
|
|
||||||
if (address == 0 || length == 0 || ulong.MaxValue - address < length - 1)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> probe = stackalloc byte[1];
|
try
|
||||||
return ctx.Memory.TryRead(address, probe) &&
|
{
|
||||||
ctx.Memory.TryRead(address + length - 1, probe);
|
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(
|
private static bool IsMappedGuestRangeAvailable(
|
||||||
@@ -4191,20 +4237,6 @@ public static class KernelMemoryCompatExports
|
|||||||
var app0Root = ResolveApp0Root();
|
var app0Root = ResolveApp0Root();
|
||||||
if (!string.IsNullOrWhiteSpace(app0Root))
|
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) ||
|
if (string.Equals(guestPath, "/app0", StringComparison.OrdinalIgnoreCase) ||
|
||||||
string.Equals(guestPath, "app0", StringComparison.OrdinalIgnoreCase))
|
string.Equals(guestPath, "app0", StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -4525,7 +4557,7 @@ public static class KernelMemoryCompatExports
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
var one = chunk.AsSpan(0, 1);
|
Span<byte> one = stackalloc byte[1];
|
||||||
if (!TryReadCompat(ctx, current, one))
|
if (!TryReadCompat(ctx, current, one))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -5867,40 +5899,22 @@ public static class KernelMemoryCompatExports
|
|||||||
private static bool TryGetAprFileSize(string hostPath, out ulong size)
|
private static bool TryGetAprFileSize(string hostPath, out ulong size)
|
||||||
{
|
{
|
||||||
size = 0;
|
size = 0;
|
||||||
|
|
||||||
string cachePath;
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
cachePath = Path.GetFullPath(hostPath);
|
var fileInfo = new FileInfo(hostPath);
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
cachePath = hostPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (_aprFileSizeCache.TryGetValue(cachePath, out size))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var fileInfo = new FileInfo(cachePath);
|
|
||||||
if (fileInfo.Exists)
|
if (fileInfo.Exists)
|
||||||
{
|
{
|
||||||
var length = fileInfo.Length;
|
var length = fileInfo.Length;
|
||||||
size = length < 0 ? 0UL : unchecked((ulong)length);
|
size = length < 0 ? 0UL : unchecked((ulong)length);
|
||||||
_aprFileSizeCache.TryAdd(cachePath, size);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!new DirectoryInfo(cachePath).Exists)
|
if (!new DirectoryInfo(hostPath).Exists)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
size = 65536;
|
size = 65536;
|
||||||
_aprFileSizeCache.TryAdd(cachePath, size);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -6199,20 +6213,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)
|
private static string PreviewIoBytes(byte[] buffer, int count, int maxBytes)
|
||||||
{
|
{
|
||||||
if (count <= 0)
|
if (count <= 0)
|
||||||
|
|||||||
@@ -95,13 +95,6 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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(
|
[SysAbiExport(
|
||||||
Nid = "T72hz6ffq08",
|
Nid = "T72hz6ffq08",
|
||||||
ExportName = "scePthreadYield",
|
ExportName = "scePthreadYield",
|
||||||
@@ -1018,12 +1011,19 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
||||||
ctx.TryWriteUInt32(address + 0x20, unchecked((uint)state.Type)) &&
|
TryWriteUInt32(ctx, address + 0x20, unchecked((uint)state.Type)) &&
|
||||||
ctx.TryWriteUInt32(address + 0x3C, unchecked((uint)state.Protocol));
|
TryWriteUInt32(ctx, address + 0x3C, unchecked((uint)state.Protocol));
|
||||||
|
|
||||||
private static bool WriteMutexAttrObject(CpuContext ctx, ulong address, PthreadMutexAttrState state) =>
|
private static bool WriteMutexAttrObject(CpuContext ctx, ulong address, PthreadMutexAttrState state) =>
|
||||||
ctx.TryWriteUInt32(address, unchecked((uint)state.Type)) &&
|
TryWriteUInt32(ctx, address, unchecked((uint)state.Type)) &&
|
||||||
ctx.TryWriteUInt32(address + 4, unchecked((uint)state.Protocol));
|
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)
|
private static int PthreadCondInitCore(CpuContext ctx, ulong condAddress)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
private const int DefaultSchedPriority = DefaultThreadPriority;
|
private const int DefaultSchedPriority = DefaultThreadPriority;
|
||||||
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
|
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
|
||||||
private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_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 object _stateGate = new();
|
||||||
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
|
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
|
||||||
@@ -32,7 +31,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
private static int _nextTlsKey = 1;
|
private static int _nextTlsKey = 1;
|
||||||
private static long _nextSyntheticRwlockHandleId = 1;
|
private static long _nextSyntheticRwlockHandleId = 1;
|
||||||
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
||||||
private static long _nextSyntheticRwlockAttrHandleId = 1;
|
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
||||||
|
|
||||||
@@ -841,37 +839,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return PthreadAttrSetstacksize(ctx);
|
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(
|
[SysAbiExport(
|
||||||
Nid = "6ULAa0fq4jA",
|
Nid = "6ULAa0fq4jA",
|
||||||
ExportName = "scePthreadRwlockInit",
|
ExportName = "scePthreadRwlockInit",
|
||||||
@@ -1049,47 +1016,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadRwlockUnlock(CpuContext ctx) => PthreadRwlockUnlock(ctx);
|
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(
|
[SysAbiExport(
|
||||||
Nid = "mqULNdimTn0",
|
Nid = "mqULNdimTn0",
|
||||||
ExportName = "pthread_key_create",
|
ExportName = "pthread_key_create",
|
||||||
@@ -1119,7 +1045,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = unchecked((uint)key);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ using System.Buffers.Binary;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
using System.IO;
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Intrinsics.X86;
|
using System.Runtime.Intrinsics.X86;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
@@ -86,11 +87,11 @@ public static class KernelRuntimeCompatExports
|
|||||||
|
|
||||||
if (micros < 1000)
|
if (micros < 1000)
|
||||||
{
|
{
|
||||||
// Guest worker pools use usleep(1) as a polling backoff. Do not turn
|
// Guest worker pools use usleep(1) as a polling backoff. Periodically
|
||||||
// PS microsecond waits into Windows millisecond sleeps on hot paths.
|
// relinquish a full host time slice so spin workers cannot starve producers.
|
||||||
if ((++_shortUsleepCount & 255) == 0)
|
if ((++_shortUsleepCount & 31) == 0)
|
||||||
{
|
{
|
||||||
Thread.Sleep(0);
|
Thread.Sleep(1);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -738,11 +739,8 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
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))
|
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
||||||
{
|
{
|
||||||
@@ -1735,16 +1733,117 @@ public static class KernelRuntimeCompatExports
|
|||||||
bool allowSearch,
|
bool allowSearch,
|
||||||
out ulong mappedAddress)
|
out ulong mappedAddress)
|
||||||
{
|
{
|
||||||
return KernelVirtualRangeAllocator.TryReserve(
|
mappedAddress = 0;
|
||||||
ctx,
|
if (length == 0)
|
||||||
desiredAddress,
|
{
|
||||||
length,
|
return false;
|
||||||
executable: false,
|
}
|
||||||
alignment,
|
|
||||||
allowSearch,
|
try
|
||||||
allowAllocateAtAlternative: allowSearch,
|
{
|
||||||
"reserve_virtual_range",
|
object memoryObject = ctx.Memory;
|
||||||
out mappedAddress);
|
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)
|
private static ulong AlignUp(ulong value, ulong alignment)
|
||||||
@@ -1757,9 +1856,4 @@ public static class KernelRuntimeCompatExports
|
|||||||
var mask = alignment - 1;
|
var mask = alignment - 1;
|
||||||
return (value + mask) & ~mask;
|
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
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -47,7 +49,7 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
|
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
@@ -66,7 +68,7 @@ public static class KernelSemaphoreCompatExports
|
|||||||
Count = initialCount,
|
Count = initialCount,
|
||||||
};
|
};
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||||
{
|
{
|
||||||
_semaphores.TryRemove(handle, out _);
|
_semaphores.TryRemove(handle, out _);
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
@@ -108,12 +110,12 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(timeoutAddress, out _))
|
if (!TryReadUInt32(ctx, timeoutAddress, out _))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
}
|
}
|
||||||
@@ -220,7 +222,7 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
lock (semaphore.Gate)
|
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);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
@@ -256,6 +258,56 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
if (address == 0 || maxLength <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var bytes = new byte[Math.Min(maxLength, 4096)];
|
||||||
|
for (var i = 0; i < bytes.Length; i++)
|
||||||
|
{
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)i, current))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, i);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[i] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceSemaphore(string message)
|
private static void TraceSemaphore(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -160,16 +160,6 @@ public static class PadExports
|
|||||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: 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)
|
private static bool WriteNeutralPadData(CpuContext ctx, ulong dataAddress)
|
||||||
{
|
{
|
||||||
Span<byte> data = stackalloc byte[PadDataSize];
|
Span<byte> data = stackalloc byte[PadDataSize];
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ public static class PlayGoExports
|
|||||||
if (outChunkIdList == 0)
|
if (outChunkIdList == 0)
|
||||||
{
|
{
|
||||||
TracePlayGo($"get_chunk_id count_only entries={availableEntries} out_entries=0x{outEntries:X16}");
|
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_OK
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
: (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}");
|
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_OK
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -503,7 +503,7 @@ public static class PlayGoExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
TracePlayGo($"get_todo requested={numberOfEntries} wrote=0");
|
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_OK
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -748,6 +748,13 @@ public static class PlayGoExports
|
|||||||
return ctx.Memory.TryWrite(address, buffer);
|
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)
|
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(int)];
|
Span<byte> buffer = stackalloc byte[sizeof(int)];
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ public static class SaveDataDialogExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_lastMode = TryReadInt32(ctx, paramAddress, out var mode) ? mode : 0;
|
_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
|
// There is no host save dialog yet. Complete immediately with OK so
|
||||||
// guest polling sees a finished dialog instead of spinning forever.
|
// guest polling sees a finished dialog instead of spinning forever.
|
||||||
@@ -182,6 +182,19 @@ public static class SaveDataDialogExports
|
|||||||
return true;
|
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)
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
|||||||
@@ -108,8 +108,8 @@ public static class SaveDataExports
|
|||||||
var setNum = result.DirNamesNum == 0
|
var setNum = result.DirNamesNum == 0
|
||||||
? 0
|
? 0
|
||||||
: Math.Min(result.DirNamesNum, entries.Count);
|
: Math.Min(result.DirNamesNum, entries.Count);
|
||||||
if (!ctx.TryWriteUInt32(resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
if (!TryWriteUInt32(ctx, resultAddress + ResultHitNumOffset, checked((uint)entries.Count)) ||
|
||||||
!ctx.TryWriteUInt32(resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
!TryWriteUInt32(ctx, resultAddress + ResultSetNumOffset, checked((uint)setNum)))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
@@ -173,9 +173,9 @@ public static class SaveDataExports
|
|||||||
!ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) ||
|
!ctx.TryReadUInt64(mountAddress + 0x08, out var dirNameAddress) ||
|
||||||
!ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) ||
|
!ctx.TryReadUInt64(mountAddress + 0x10, out var blocks) ||
|
||||||
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
|
!ctx.TryReadUInt64(mountAddress + 0x18, out var systemBlocks) ||
|
||||||
!ctx.TryReadUInt32(mountAddress + 0x20, out var mountMode) ||
|
!TryReadUInt32(ctx, mountAddress + 0x20, out var mountMode) ||
|
||||||
!ctx.TryReadUInt32(mountAddress + 0x24, out var resource) ||
|
!TryReadUInt32(ctx, mountAddress + 0x24, out var resource) ||
|
||||||
!ctx.TryReadUInt32(mountAddress + 0x28, out var mode) ||
|
!TryReadUInt32(ctx, mountAddress + 0x28, out var mode) ||
|
||||||
dirNameAddress == 0 ||
|
dirNameAddress == 0 ||
|
||||||
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
!TryReadFixedAscii(ctx, dirNameAddress, SaveDataDirNameSize, out var dirName))
|
||||||
{
|
{
|
||||||
@@ -244,44 +244,14 @@ public static class SaveDataExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
private static bool TryReadSearchCond(CpuContext ctx, ulong address, out SearchCond cond)
|
||||||
{
|
{
|
||||||
cond = default;
|
cond = default;
|
||||||
if (!TryReadInt32(ctx, address, out var userId) ||
|
if (!TryReadInt32(ctx, address, out var userId) ||
|
||||||
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
!ctx.TryReadUInt64(address + 0x08, out var titleIdAddress) ||
|
||||||
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
!ctx.TryReadUInt64(address + 0x10, out var dirNameAddress) ||
|
||||||
!ctx.TryReadUInt32(address + 0x18, out var sortKey) ||
|
!TryReadUInt32(ctx, address + 0x18, out var sortKey) ||
|
||||||
!ctx.TryReadUInt32(address + 0x1C, out var sortOrder))
|
!TryReadUInt32(ctx, address + 0x1C, out var sortOrder))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -304,7 +274,7 @@ public static class SaveDataExports
|
|||||||
{
|
{
|
||||||
result = default;
|
result = default;
|
||||||
if (!ctx.TryReadUInt64(address + ResultDirNamesOffset, out var dirNamesAddress) ||
|
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 + ResultParamsOffset, out var paramsAddress) ||
|
||||||
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
|
!ctx.TryReadUInt64(address + ResultInfosOffset, out var infosAddress))
|
||||||
{
|
{
|
||||||
@@ -518,6 +488,26 @@ public static class SaveDataExports
|
|||||||
return true;
|
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)
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
{
|
{
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
|||||||
@@ -10,15 +10,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Silk.NET.Vulkan" />
|
<PackageReference Include="Silk.NET.Vulkan" />
|
||||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.EXT" />
|
|
||||||
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
<PackageReference Include="Silk.NET.Vulkan.Extensions.KHR" />
|
||||||
<PackageReference Include="Silk.NET.Windowing" />
|
<PackageReference Include="Silk.NET.Windowing" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -922,9 +922,9 @@ public static class VideoOutExports
|
|||||||
private static bool TryReadBufferAttribute(CpuContext ctx, ulong attributeAddress, bool attribute2, out BufferAttribute attribute)
|
private static bool TryReadBufferAttribute(CpuContext ctx, ulong attributeAddress, bool attribute2, out BufferAttribute attribute)
|
||||||
{
|
{
|
||||||
attribute = default;
|
attribute = default;
|
||||||
if (!ctx.TryReadUInt32(attributeAddress + 0x04, out var tilingMode) ||
|
if (!TryReadUInt32(ctx, attributeAddress + 0x04, out var tilingMode) ||
|
||||||
!ctx.TryReadUInt32(attributeAddress + 0x0C, out var width) ||
|
!TryReadUInt32(ctx, attributeAddress + 0x0C, out var width) ||
|
||||||
!ctx.TryReadUInt32(attributeAddress + 0x10, out var height))
|
!TryReadUInt32(ctx, attributeAddress + 0x10, out var height))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -941,10 +941,10 @@ public static class VideoOutExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(attributeAddress + 0x00, out var pixelFormat32) ||
|
if (!TryReadUInt32(ctx, attributeAddress + 0x00, out var pixelFormat32) ||
|
||||||
!ctx.TryReadUInt32(attributeAddress + 0x08, out var aspectRatio) ||
|
!TryReadUInt32(ctx, attributeAddress + 0x08, out var aspectRatio) ||
|
||||||
!ctx.TryReadUInt32(attributeAddress + 0x14, out var pitchInPixel) ||
|
!TryReadUInt32(ctx, attributeAddress + 0x14, out var pitchInPixel) ||
|
||||||
!ctx.TryReadUInt32(attributeAddress + 0x18, out var option32))
|
!TryReadUInt32(ctx, attributeAddress + 0x18, out var option32))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1291,6 +1291,19 @@ public static class VideoOutExports
|
|||||||
return true;
|
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)
|
private static bool TryReadInt16(CpuContext ctx, ulong address, out short value)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(short)];
|
Span<byte> buffer = stackalloc byte[sizeof(short)];
|
||||||
|
|||||||
@@ -7,7 +7,6 @@ using Silk.NET.Maths;
|
|||||||
using SharpEmu.Libs.Agc;
|
using SharpEmu.Libs.Agc;
|
||||||
using Silk.NET.Vulkan;
|
using Silk.NET.Vulkan;
|
||||||
using Silk.NET.Vulkan.Extensions.KHR;
|
using Silk.NET.Vulkan.Extensions.KHR;
|
||||||
using Silk.NET.Vulkan.Extensions.EXT;
|
|
||||||
using Silk.NET.Windowing;
|
using Silk.NET.Windowing;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
using System.Runtime.CompilerServices;
|
using System.Runtime.CompilerServices;
|
||||||
@@ -847,8 +846,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private delegate* unmanaged<CommandBuffer, void> _cmdEndDebugUtilsLabel;
|
private delegate* unmanaged<CommandBuffer, void> _cmdEndDebugUtilsLabel;
|
||||||
private Instance _instance;
|
private Instance _instance;
|
||||||
private SurfaceKHR _surface;
|
private SurfaceKHR _surface;
|
||||||
private DebugUtilsMessengerEXT _debugMessenger;
|
|
||||||
private ExtDebugUtils? _debugUtils;
|
|
||||||
private PhysicalDevice _physicalDevice;
|
private PhysicalDevice _physicalDevice;
|
||||||
private Device _device;
|
private Device _device;
|
||||||
private Queue _queue;
|
private Queue _queue;
|
||||||
@@ -879,8 +876,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private bool _swapchainRecreateDeferred;
|
private bool _swapchainRecreateDeferred;
|
||||||
private bool _tracedPresentedSwapchain;
|
private bool _tracedPresentedSwapchain;
|
||||||
private bool _swapchainReadbackPending;
|
private bool _swapchainReadbackPending;
|
||||||
private bool _deviceLost;
|
|
||||||
private bool _deviceLostLogged;
|
|
||||||
private int _directPresentationCount;
|
private int _directPresentationCount;
|
||||||
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
|
private readonly Dictionary<ulong, GuestImageResource> _guestImages = new();
|
||||||
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new();
|
private readonly HashSet<(ulong Address, uint Width, uint Height, Format Format)> _tracedTextureCacheHits = new();
|
||||||
@@ -1246,9 +1241,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private void CreateInstance()
|
private void CreateInstance()
|
||||||
{
|
{
|
||||||
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
var applicationName = (byte*)SilkMarshal.StringToPtr("SharpEmu");
|
||||||
var enableValidation = Environment.GetEnvironmentVariable("SHARPEMU_VK_VALIDATION") == "1";
|
|
||||||
byte* validationLayerName = null;
|
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var applicationInfo = new ApplicationInfo
|
var applicationInfo = new ApplicationInfo
|
||||||
@@ -1276,29 +1268,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
enabledExtensions[enabledExtensionCount++] = debugUtilsExtension;
|
enabledExtensions[enabledExtensionCount++] = debugUtilsExtension;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (enableValidation && IsInstanceLayerAvailable("VK_LAYER_KHRONOS_validation"))
|
|
||||||
{
|
|
||||||
validationLayerName = (byte*)SilkMarshal.StringToPtr("VK_LAYER_KHRONOS_validation");
|
|
||||||
}
|
|
||||||
else if (enableValidation)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine("[LOADER][WARN] SHARPEMU_VK_VALIDATION=1 but VK_LAYER_KHRONOS_validation not found (Vulkan SDK installed?).");
|
|
||||||
}
|
|
||||||
|
|
||||||
var layers = stackalloc byte*[1];
|
|
||||||
if (validationLayerName is not null)
|
|
||||||
{
|
|
||||||
layers[0] = validationLayerName;
|
|
||||||
}
|
|
||||||
|
|
||||||
var createInfo = new InstanceCreateInfo
|
var createInfo = new InstanceCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.InstanceCreateInfo,
|
SType = StructureType.InstanceCreateInfo,
|
||||||
PApplicationInfo = &applicationInfo,
|
PApplicationInfo = &applicationInfo,
|
||||||
EnabledExtensionCount = (uint)enabledExtensionCount,
|
EnabledExtensionCount = (uint)enabledExtensionCount,
|
||||||
PpEnabledExtensionNames = enabledExtensions,
|
PpEnabledExtensionNames = enabledExtensions,
|
||||||
EnabledLayerCount = validationLayerName is not null ? 1u : 0u,
|
|
||||||
PpEnabledLayerNames = validationLayerName is not null ? layers : null,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -1308,13 +1283,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
|
throw new InvalidOperationException("VK_KHR_surface is unavailable.");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (validationLayerName is not null && _vk.TryGetInstanceExtension(_instance, out ExtDebugUtils debugUtils))
|
|
||||||
{
|
|
||||||
_debugUtils = debugUtils;
|
|
||||||
RegisterDebugMessenger(debugUtils);
|
|
||||||
Console.Error.WriteLine("[LOADER][INFO] Vulkan Validation Layers active (SHARPEMU_VK_VALIDATION=1).");
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -1327,74 +1295,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
SilkMarshal.Free((nint)applicationName);
|
SilkMarshal.Free((nint)applicationName);
|
||||||
if (validationLayerName is not null)
|
|
||||||
{
|
|
||||||
SilkMarshal.Free((nint)validationLayerName);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool IsInstanceLayerAvailable(string layerName)
|
|
||||||
{
|
|
||||||
uint layerCount = 0;
|
|
||||||
if (_vk.EnumerateInstanceLayerProperties(&layerCount, null) != Result.Success || layerCount == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var properties = new LayerProperties[layerCount];
|
|
||||||
fixed (LayerProperties* propertyPointer = properties)
|
|
||||||
{
|
|
||||||
if (_vk.EnumerateInstanceLayerProperties(&layerCount, propertyPointer) != Result.Success)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expected = Encoding.UTF8.GetBytes(layerName);
|
|
||||||
for (var index = 0; index < layerCount; index++)
|
|
||||||
{
|
|
||||||
if (Utf8NullTerminatedEquals(propertyPointer[index].LayerName, expected))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
private void RegisterDebugMessenger(ExtDebugUtils debugUtils)
|
|
||||||
{
|
|
||||||
var messengerInfo = new DebugUtilsMessengerCreateInfoEXT
|
|
||||||
{
|
|
||||||
SType = StructureType.DebugUtilsMessengerCreateInfoExt,
|
|
||||||
MessageSeverity = DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt
|
|
||||||
| DebugUtilsMessageSeverityFlagsEXT.WarningBitExt,
|
|
||||||
MessageType = DebugUtilsMessageTypeFlagsEXT.ValidationBitExt
|
|
||||||
| DebugUtilsMessageTypeFlagsEXT.PerformanceBitExt
|
|
||||||
| DebugUtilsMessageTypeFlagsEXT.GeneralBitExt,
|
|
||||||
PfnUserCallback = new PfnDebugUtilsMessengerCallbackEXT(DebugCallback),
|
|
||||||
};
|
|
||||||
|
|
||||||
Check(debugUtils.CreateDebugUtilsMessenger(_instance, &messengerInfo, null, out _debugMessenger),
|
|
||||||
"vkCreateDebugUtilsMessengerEXT");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static unsafe uint DebugCallback(
|
|
||||||
DebugUtilsMessageSeverityFlagsEXT severity,
|
|
||||||
DebugUtilsMessageTypeFlagsEXT type,
|
|
||||||
DebugUtilsMessengerCallbackDataEXT* callbackData,
|
|
||||||
void* userData)
|
|
||||||
{
|
|
||||||
var message = SilkMarshal.PtrToString((nint)callbackData->PMessage);
|
|
||||||
var prefix = severity switch
|
|
||||||
{
|
|
||||||
DebugUtilsMessageSeverityFlagsEXT.ErrorBitExt => "[VULKAN][ERROR]",
|
|
||||||
DebugUtilsMessageSeverityFlagsEXT.WarningBitExt => "[VULKAN][WARN]",
|
|
||||||
_ => "[VULKAN][INFO]",
|
|
||||||
};
|
|
||||||
Console.Error.WriteLine($"{prefix} {message}");
|
|
||||||
return Vk.False;
|
|
||||||
}
|
|
||||||
private void CreateSurface()
|
private void CreateSurface()
|
||||||
{
|
{
|
||||||
var instanceHandle = new VkHandle(_instance.Handle);
|
var instanceHandle = new VkHandle(_instance.Handle);
|
||||||
@@ -1455,129 +1358,17 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
QueueCount = 1,
|
QueueCount = 1,
|
||||||
PQueuePriorities = &priority,
|
PQueuePriorities = &priority,
|
||||||
};
|
};
|
||||||
_vk.GetPhysicalDeviceFeatures(_physicalDevice, out var supportedFeatures);
|
|
||||||
var enabledFeatures = new PhysicalDeviceFeatures
|
|
||||||
{
|
|
||||||
VertexPipelineStoresAndAtomics = supportedFeatures.VertexPipelineStoresAndAtomics,
|
|
||||||
FragmentStoresAndAtomics = supportedFeatures.FragmentStoresAndAtomics,
|
|
||||||
ShaderInt64 = supportedFeatures.ShaderInt64,
|
|
||||||
ShaderImageGatherExtended = supportedFeatures.ShaderImageGatherExtended,
|
|
||||||
ShaderStorageImageExtendedFormats = supportedFeatures.ShaderStorageImageExtendedFormats,
|
|
||||||
ShaderStorageImageReadWithoutFormat = supportedFeatures.ShaderStorageImageReadWithoutFormat,
|
|
||||||
ShaderStorageImageWriteWithoutFormat = supportedFeatures.ShaderStorageImageWriteWithoutFormat,
|
|
||||||
RobustBufferAccess = supportedFeatures.RobustBufferAccess,
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!supportedFeatures.RobustBufferAccess)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] GPU does not support robustBufferAccess " +
|
|
||||||
"translated shaders performing out-of-bounds buffer access may cause device loss.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supportedFeatures.ShaderInt64)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] GPU does not support shaderInt64 " +
|
|
||||||
"translated shaders using 64-bit integers will fail.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supportedFeatures.VertexPipelineStoresAndAtomics || !supportedFeatures.FragmentStoresAndAtomics)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] GPU does not support vertexPipelineStoresAndAtomics/fragmentStoresAndAtomics " +
|
|
||||||
"translated shaders using storage buffers in vertex/fragment stages may fail.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supportedFeatures.ShaderImageGatherExtended)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] GPU does not support shaderImageGatherExtended " +
|
|
||||||
"translated shaders using image gather with offsets/LOD/bias will fail.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supportedFeatures.ShaderStorageImageReadWithoutFormat ||
|
|
||||||
!supportedFeatures.ShaderStorageImageWriteWithoutFormat)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] GPU does not support shaderStorageImage(Read|Write)WithoutFormat " +
|
|
||||||
"translated shaders using unformatted storage image load/store will fail.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var maintenance8Features = new PhysicalDeviceMaintenance8FeaturesKHR
|
|
||||||
{
|
|
||||||
SType = StructureType.PhysicalDeviceMaintenance8FeaturesKhr,
|
|
||||||
};
|
|
||||||
var robustness2Features = new PhysicalDeviceRobustness2FeaturesEXT
|
|
||||||
{
|
|
||||||
SType = StructureType.PhysicalDeviceRobustness2FeaturesExt,
|
|
||||||
PNext = &maintenance8Features,
|
|
||||||
};
|
|
||||||
var featuresQuery = new PhysicalDeviceFeatures2
|
|
||||||
{
|
|
||||||
SType = StructureType.PhysicalDeviceFeatures2,
|
|
||||||
PNext = &robustness2Features,
|
|
||||||
};
|
|
||||||
_vk.GetPhysicalDeviceFeatures2(_physicalDevice, &featuresQuery);
|
|
||||||
var supportsMaintenance8 = maintenance8Features.Maintenance8;
|
|
||||||
var supportsRobustImageAccess2 = robustness2Features.RobustImageAccess2;
|
|
||||||
var supportsNullDescriptor = robustness2Features.NullDescriptor;
|
|
||||||
var supportsRobustness2 = supportsRobustImageAccess2 || supportsNullDescriptor;
|
|
||||||
if (!supportsMaintenance8)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] GPU does not support VK_KHR_maintenance8 " +
|
|
||||||
"translated shaders using a dynamic texel offset on non-gather image samples will fail.");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!supportsRobustImageAccess2)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][WARN] GPU does not support VK_EXT_robustness2 robustImageAccess2 " +
|
|
||||||
"translated shaders performing out-of-bounds image access may cause device loss.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var swapchainExtension = (byte*)SilkMarshal.StringToPtr("VK_KHR_swapchain");
|
var swapchainExtension = (byte*)SilkMarshal.StringToPtr("VK_KHR_swapchain");
|
||||||
var maintenance8Extension = (byte*)SilkMarshal.StringToPtr("VK_KHR_maintenance8");
|
|
||||||
var robustness2Extension = (byte*)SilkMarshal.StringToPtr("VK_EXT_robustness2");
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var extensions = stackalloc byte*[3];
|
|
||||||
var extensionCount = 0u;
|
|
||||||
extensions[extensionCount++] = swapchainExtension;
|
|
||||||
if (supportsMaintenance8)
|
|
||||||
{
|
|
||||||
extensions[extensionCount++] = maintenance8Extension;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (supportsRobustness2)
|
|
||||||
{
|
|
||||||
extensions[extensionCount++] = robustness2Extension;
|
|
||||||
}
|
|
||||||
|
|
||||||
maintenance8Features.Maintenance8 = supportsMaintenance8;
|
|
||||||
maintenance8Features.PNext = null;
|
|
||||||
robustness2Features.RobustBufferAccess2 =
|
|
||||||
supportsRobustImageAccess2 && supportedFeatures.RobustBufferAccess;
|
|
||||||
robustness2Features.RobustImageAccess2 = supportsRobustImageAccess2;
|
|
||||||
robustness2Features.NullDescriptor = supportsNullDescriptor;
|
|
||||||
robustness2Features.PNext = supportsMaintenance8 ? &maintenance8Features : null;
|
|
||||||
var features2 = new PhysicalDeviceFeatures2
|
|
||||||
{
|
|
||||||
SType = StructureType.PhysicalDeviceFeatures2,
|
|
||||||
PNext = supportsRobustness2
|
|
||||||
? &robustness2Features
|
|
||||||
: (supportsMaintenance8 ? &maintenance8Features : null),
|
|
||||||
Features = enabledFeatures,
|
|
||||||
};
|
|
||||||
var createInfo = new DeviceCreateInfo
|
var createInfo = new DeviceCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.DeviceCreateInfo,
|
SType = StructureType.DeviceCreateInfo,
|
||||||
PNext = &features2,
|
|
||||||
QueueCreateInfoCount = 1,
|
QueueCreateInfoCount = 1,
|
||||||
PQueueCreateInfos = &queueInfo,
|
PQueueCreateInfos = &queueInfo,
|
||||||
EnabledExtensionCount = extensionCount,
|
EnabledExtensionCount = 1,
|
||||||
PpEnabledExtensionNames = extensions,
|
PpEnabledExtensionNames = &swapchainExtension,
|
||||||
};
|
};
|
||||||
|
|
||||||
Check(_vk.CreateDevice(_physicalDevice, &createInfo, null, out _device), "vkCreateDevice");
|
Check(_vk.CreateDevice(_physicalDevice, &createInfo, null, out _device), "vkCreateDevice");
|
||||||
@@ -1585,8 +1376,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
SilkMarshal.Free((nint)swapchainExtension);
|
SilkMarshal.Free((nint)swapchainExtension);
|
||||||
SilkMarshal.Free((nint)maintenance8Extension);
|
|
||||||
SilkMarshal.Free((nint)robustness2Extension);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);
|
_vk.GetDeviceQueue(_device, _queueFamilyIndex, 0, out _queue);
|
||||||
@@ -1751,39 +1540,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
resources.DebugName));
|
resources.DebugName));
|
||||||
}
|
}
|
||||||
|
|
||||||
private void SubmitGuestCommandBufferAndWait(CommandBuffer commandBuffer)
|
|
||||||
{
|
|
||||||
var fenceInfo = new FenceCreateInfo
|
|
||||||
{
|
|
||||||
SType = StructureType.FenceCreateInfo,
|
|
||||||
};
|
|
||||||
Fence fence;
|
|
||||||
Check(
|
|
||||||
_vk.CreateFence(_device, &fenceInfo, null, out fence),
|
|
||||||
"vkCreateFence(guest chunk)");
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var submitInfo = new SubmitInfo
|
|
||||||
{
|
|
||||||
SType = StructureType.SubmitInfo,
|
|
||||||
CommandBufferCount = 1,
|
|
||||||
PCommandBuffers = &commandBuffer,
|
|
||||||
};
|
|
||||||
Check(
|
|
||||||
_vk.QueueSubmit(_queue, 1, &submitInfo, fence),
|
|
||||||
"vkQueueSubmit(guest chunk)");
|
|
||||||
Check(
|
|
||||||
_vk.WaitForFences(_device, 1, &fence, true, ulong.MaxValue),
|
|
||||||
"vkWaitForFences(guest chunk)");
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
_vk.DestroyFence(_device, fence, null);
|
|
||||||
}
|
|
||||||
|
|
||||||
_vk.FreeCommandBuffers(_device, _commandPool, 1, &commandBuffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
private void EnsureGuestSubmissionCapacity()
|
private void EnsureGuestSubmissionCapacity()
|
||||||
{
|
{
|
||||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||||
@@ -2768,7 +2524,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var pipelineInfo = new ComputePipelineCreateInfo
|
var pipelineInfo = new ComputePipelineCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.ComputePipelineCreateInfo,
|
SType = StructureType.ComputePipelineCreateInfo,
|
||||||
Flags = PipelineCreateFlags.CreateDispatchBaseBit,
|
|
||||||
Stage = stage,
|
Stage = stage,
|
||||||
Layout = resources.PipelineLayout,
|
Layout = resources.PipelineLayout,
|
||||||
};
|
};
|
||||||
@@ -2818,8 +2573,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
dstSelect: texture.DstSelect,
|
dstSelect: texture.DstSelect,
|
||||||
out var view))
|
out var view))
|
||||||
{
|
{
|
||||||
if (ShouldTraceVulkanResources() &&
|
if (_tracedTextureCacheHits.Add(
|
||||||
_tracedTextureCacheHits.Add(
|
|
||||||
(texture.Address, texture.Width, texture.Height, vkFormat)))
|
(texture.Address, texture.Width, texture.Height, vkFormat)))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -3115,13 +2869,9 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
_vk.UnmapMemory(_device, stagingMemory);
|
_vk.UnmapMemory(_device, stagingMemory);
|
||||||
|
|
||||||
var supportsAttachmentUsage = !IsBlockCompressedFormat(vkFormat);
|
|
||||||
var imageInfo = new ImageCreateInfo
|
var imageInfo = new ImageCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.ImageCreateInfo,
|
SType = StructureType.ImageCreateInfo,
|
||||||
Flags = supportsAttachmentUsage
|
|
||||||
? ImageCreateFlags.CreateMutableFormatBit | ImageCreateFlags.CreateExtendedUsageBit
|
|
||||||
: 0,
|
|
||||||
ImageType = ImageType.Type2D,
|
ImageType = ImageType.Type2D,
|
||||||
Format = vkFormat,
|
Format = vkFormat,
|
||||||
Extent = new Extent3D(width, height, 1),
|
Extent = new Extent3D(width, height, 1),
|
||||||
@@ -3129,13 +2879,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
ArrayLayers = 1,
|
ArrayLayers = 1,
|
||||||
Samples = SampleCountFlags.Count1Bit,
|
Samples = SampleCountFlags.Count1Bit,
|
||||||
Tiling = ImageTiling.Optimal,
|
Tiling = ImageTiling.Optimal,
|
||||||
Usage = supportsAttachmentUsage
|
Usage = ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit,
|
||||||
? ImageUsageFlags.TransferDstBit |
|
|
||||||
ImageUsageFlags.SampledBit |
|
|
||||||
ImageUsageFlags.ColorAttachmentBit |
|
|
||||||
ImageUsageFlags.StorageBit |
|
|
||||||
ImageUsageFlags.TransferSrcBit
|
|
||||||
: ImageUsageFlags.TransferDstBit | ImageUsageFlags.SampledBit,
|
|
||||||
SharingMode = SharingMode.Exclusive,
|
SharingMode = SharingMode.Exclusive,
|
||||||
InitialLayout = ImageLayout.Undefined,
|
InitialLayout = ImageLayout.Undefined,
|
||||||
};
|
};
|
||||||
@@ -3398,8 +3142,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
out var memory);
|
out var memory);
|
||||||
var size = (ulong)Math.Max(guestBuffer.Data.Length, sizeof(uint));
|
var size = (ulong)Math.Max(guestBuffer.Data.Length, sizeof(uint));
|
||||||
|
|
||||||
if (ShouldTraceVulkanResources() &&
|
if (_tracedGlobalBuffers.Add((guestBuffer.BaseAddress, guestBuffer.Data.Length)))
|
||||||
_tracedGlobalBuffers.Add((guestBuffer.BaseAddress, guestBuffer.Data.Length)))
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
|
$"[LOADER][TRACE] vk.global_buffer base=0x{guestBuffer.BaseAddress:X16} " +
|
||||||
@@ -3971,12 +3714,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_ => Format.Undefined,
|
_ => Format.Undefined,
|
||||||
};
|
};
|
||||||
|
|
||||||
private static bool IsBlockCompressedFormat(Format format) =>
|
|
||||||
format is Format.BC1RgbaUnormBlock or
|
|
||||||
Format.BC1RgbaSrgbBlock or
|
|
||||||
Format.BC7UnormBlock or
|
|
||||||
Format.BC7SrgbBlock;
|
|
||||||
|
|
||||||
private VkBuffer CreateBuffer(
|
private VkBuffer CreateBuffer(
|
||||||
ulong size,
|
ulong size,
|
||||||
BufferUsageFlags usage,
|
BufferUsageFlags usage,
|
||||||
@@ -4030,24 +3767,8 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
throw new InvalidOperationException("No compatible Vulkan host-visible memory type was found.");
|
throw new InvalidOperationException("No compatible Vulkan host-visible memory type was found.");
|
||||||
}
|
}
|
||||||
|
|
||||||
private const uint MaxComputeZSlicesPerSubmission = 8;
|
|
||||||
|
|
||||||
private void ExecuteComputeDispatch(VulkanComputeGuestDispatch work)
|
private void ExecuteComputeDispatch(VulkanComputeGuestDispatch work)
|
||||||
{
|
{
|
||||||
if (_deviceLost)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (AddressListContains("SHARPEMU_SKIP_COMPUTE_CS", work.ShaderAddress))
|
|
||||||
{
|
|
||||||
TraceVulkanShader(
|
|
||||||
$"vk.compute_skip cs=0x{work.ShaderAddress:X16} " +
|
|
||||||
$"groups={work.GroupCountX}x{work.GroupCountY}x{work.GroupCountZ} " +
|
|
||||||
$"textures={work.Textures.Count}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TranslatedDrawResources? resources = null;
|
TranslatedDrawResources? resources = null;
|
||||||
CommandBuffer commandBuffer = default;
|
CommandBuffer commandBuffer = default;
|
||||||
var submitted = false;
|
var submitted = false;
|
||||||
@@ -4055,100 +3776,59 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
{
|
{
|
||||||
EnsureGuestSubmissionCapacity();
|
EnsureGuestSubmissionCapacity();
|
||||||
resources = CreateComputeDispatchResources(work);
|
resources = CreateComputeDispatchResources(work);
|
||||||
|
commandBuffer = AllocateGuestCommandBuffer();
|
||||||
var batchCount = Math.Max(
|
_commandBuffer = commandBuffer;
|
||||||
1u,
|
var beginInfo = new CommandBufferBeginInfo
|
||||||
(uint)Math.Ceiling(work.GroupCountZ / (double)MaxComputeZSlicesPerSubmission));
|
|
||||||
|
|
||||||
for (var batchIndex = 0u; batchIndex < batchCount; batchIndex++)
|
|
||||||
{
|
{
|
||||||
var zStart = batchIndex * MaxComputeZSlicesPerSubmission;
|
SType = StructureType.CommandBufferBeginInfo,
|
||||||
var zCount = Math.Min(MaxComputeZSlicesPerSubmission, work.GroupCountZ - zStart);
|
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
||||||
var isFirstBatch = batchIndex == 0;
|
};
|
||||||
var isLastBatch = batchIndex == batchCount - 1;
|
Check(
|
||||||
|
_vk.BeginCommandBuffer(_commandBuffer, &beginInfo),
|
||||||
|
"vkBeginCommandBuffer(compute)");
|
||||||
|
|
||||||
commandBuffer = AllocateGuestCommandBuffer();
|
BeginDebugLabel(_commandBuffer, resources.DebugName);
|
||||||
_commandBuffer = commandBuffer;
|
RecordTextureUploads(resources, PipelineStageFlags.ComputeShaderBit);
|
||||||
var beginInfo = new CommandBufferBeginInfo
|
RecordStorageImagesForWrite(resources, PipelineStageFlags.ComputeShaderBit);
|
||||||
{
|
_vk.CmdBindPipeline(
|
||||||
SType = StructureType.CommandBufferBeginInfo,
|
_commandBuffer,
|
||||||
Flags = CommandBufferUsageFlags.OneTimeSubmitBit,
|
PipelineBindPoint.Compute,
|
||||||
};
|
resources.Pipeline);
|
||||||
Check(
|
if (resources.DescriptorSet.Handle != 0)
|
||||||
_vk.BeginCommandBuffer(_commandBuffer, &beginInfo),
|
{
|
||||||
"vkBeginCommandBuffer(compute)");
|
var descriptorSet = resources.DescriptorSet;
|
||||||
|
_vk.CmdBindDescriptorSets(
|
||||||
BeginDebugLabel(_commandBuffer, resources.DebugName);
|
|
||||||
if (isFirstBatch)
|
|
||||||
{
|
|
||||||
RecordTextureUploads(resources, PipelineStageFlags.ComputeShaderBit);
|
|
||||||
RecordStorageImagesForWrite(resources, PipelineStageFlags.ComputeShaderBit);
|
|
||||||
}
|
|
||||||
|
|
||||||
_vk.CmdBindPipeline(
|
|
||||||
_commandBuffer,
|
_commandBuffer,
|
||||||
PipelineBindPoint.Compute,
|
PipelineBindPoint.Compute,
|
||||||
resources.Pipeline);
|
resources.PipelineLayout,
|
||||||
if (resources.DescriptorSet.Handle != 0)
|
0,
|
||||||
{
|
1,
|
||||||
var descriptorSet = resources.DescriptorSet;
|
&descriptorSet,
|
||||||
_vk.CmdBindDescriptorSets(
|
0,
|
||||||
_commandBuffer,
|
null);
|
||||||
PipelineBindPoint.Compute,
|
|
||||||
resources.PipelineLayout,
|
|
||||||
0,
|
|
||||||
1,
|
|
||||||
&descriptorSet,
|
|
||||||
0,
|
|
||||||
null);
|
|
||||||
}
|
|
||||||
|
|
||||||
RecordChunkedComputeDispatch(_commandBuffer, work, zStart, zCount);
|
|
||||||
|
|
||||||
if (isLastBatch)
|
|
||||||
{
|
|
||||||
RecordStorageImagesForRead(resources, PipelineStageFlags.ComputeShaderBit);
|
|
||||||
}
|
|
||||||
|
|
||||||
EndDebugLabel(_commandBuffer);
|
|
||||||
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(compute)");
|
|
||||||
|
|
||||||
TraceVulkanShader(
|
|
||||||
$"vk.compute_submit cs=0x{work.ShaderAddress:X16} " +
|
|
||||||
$"batch={batchIndex}/{batchCount} z={zStart}..{zStart + zCount}");
|
|
||||||
if (isLastBatch)
|
|
||||||
{
|
|
||||||
SubmitGuestCommandBuffer(
|
|
||||||
commandBuffer,
|
|
||||||
resources,
|
|
||||||
GetTraceImages(resources));
|
|
||||||
submitted = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
SubmitGuestCommandBufferAndWait(commandBuffer);
|
|
||||||
commandBuffer = default;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
RecordChunkedComputeDispatch(_commandBuffer, work);
|
||||||
|
RecordStorageImagesForRead(resources, PipelineStageFlags.ComputeShaderBit);
|
||||||
|
EndDebugLabel(_commandBuffer);
|
||||||
|
|
||||||
|
Check(_vk.EndCommandBuffer(_commandBuffer), "vkEndCommandBuffer(compute)");
|
||||||
|
SubmitGuestCommandBuffer(
|
||||||
|
commandBuffer,
|
||||||
|
resources,
|
||||||
|
GetTraceImages(resources));
|
||||||
|
submitted = true;
|
||||||
MarkSampledImagesInitialized(resources);
|
MarkSampledImagesInitialized(resources);
|
||||||
MarkStorageImagesInitialized(resources, traceContents: false);
|
MarkStorageImagesInitialized(resources, traceContents: false);
|
||||||
TraceVulkanShader(
|
TraceVulkanShader(
|
||||||
$"vk.compute_dispatch groups={work.GroupCountX}x" +
|
$"vk.compute_dispatch groups={work.GroupCountX}x" +
|
||||||
$"{work.GroupCountY}x{work.GroupCountZ} " +
|
$"{work.GroupCountY}x{work.GroupCountZ} " +
|
||||||
$"textures={work.Textures.Count} cs=0x{work.ShaderAddress:X16} " +
|
$"textures={work.Textures.Count} cs=0x{work.ShaderAddress:X16}");
|
||||||
$"batches={batchCount}");
|
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
if (TryMarkDeviceLost(exception))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][ERROR] Vulkan compute dispatch failed " +
|
$"[LOADER][ERROR] Vulkan compute dispatch failed: {exception.Message}");
|
||||||
$"cs=0x{work.ShaderAddress:X16}: {exception.Message}");
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -4171,9 +3851,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
|
|
||||||
private void RecordChunkedComputeDispatch(
|
private void RecordChunkedComputeDispatch(
|
||||||
CommandBuffer commandBuffer,
|
CommandBuffer commandBuffer,
|
||||||
VulkanComputeGuestDispatch work,
|
VulkanComputeGuestDispatch work)
|
||||||
uint zStart,
|
|
||||||
uint zCount)
|
|
||||||
{
|
{
|
||||||
const uint maxWorkgroupsPerCommand = 4096;
|
const uint maxWorkgroupsPerCommand = 4096;
|
||||||
var yChunk = Math.Max(
|
var yChunk = Math.Max(
|
||||||
@@ -4183,7 +3861,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
maxWorkgroupsPerCommand / Math.Max(work.GroupCountX, 1u)));
|
maxWorkgroupsPerCommand / Math.Max(work.GroupCountX, 1u)));
|
||||||
var commandCount = 0u;
|
var commandCount = 0u;
|
||||||
|
|
||||||
for (var z = zStart; z < zStart + zCount; z++)
|
for (var z = 0u; z < work.GroupCountZ; z++)
|
||||||
{
|
{
|
||||||
for (var y = 0u; y < work.GroupCountY; y += yChunk)
|
for (var y = 0u; y < work.GroupCountY; y += yChunk)
|
||||||
{
|
{
|
||||||
@@ -4205,17 +3883,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
TraceVulkanShader(
|
TraceVulkanShader(
|
||||||
$"vk.compute_chunked cs=0x{work.ShaderAddress:X16} " +
|
$"vk.compute_chunked cs=0x{work.ShaderAddress:X16} " +
|
||||||
$"groups={work.GroupCountX}x{work.GroupCountY}x{work.GroupCountZ} " +
|
$"groups={work.GroupCountX}x{work.GroupCountY}x{work.GroupCountZ} " +
|
||||||
$"z_range={zStart}..{zStart + zCount} commands={commandCount} y_chunk={yChunk}");
|
$"commands={commandCount} y_chunk={yChunk}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work)
|
private void ExecuteOffscreenDraw(VulkanOffscreenGuestDraw work)
|
||||||
{
|
{
|
||||||
if (_deviceLost)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
var format = GetRenderTargetFormat(work.Target.Format, work.Target.NumberType);
|
var format = GetRenderTargetFormat(work.Target.Format, work.Target.NumberType);
|
||||||
if (format == Format.Undefined)
|
if (format == Format.Undefined)
|
||||||
{
|
{
|
||||||
@@ -4373,11 +4046,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
{
|
{
|
||||||
if (TryMarkDeviceLost(exception))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!_guestImages.TryGetValue(work.Target.Address, out var failedTarget) ||
|
if (!_guestImages.TryGetValue(work.Target.Address, out var failedTarget) ||
|
||||||
!failedTarget.Initialized)
|
!failedTarget.Initialized)
|
||||||
{
|
{
|
||||||
@@ -4423,23 +4091,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
existing.MipLevels == mipLevels &&
|
existing.MipLevels == mipLevels &&
|
||||||
existing.Format == format)
|
existing.Format == format)
|
||||||
{
|
{
|
||||||
if (existing.RenderPass.Handle == 0)
|
|
||||||
{
|
|
||||||
var attachmentView = existing.MipViews.Length > 0
|
|
||||||
? existing.MipViews[0]
|
|
||||||
: existing.View;
|
|
||||||
var (promotedRenderPass, promotedFramebuffer) = CreateRenderPassAndFramebuffer(
|
|
||||||
existing.Format,
|
|
||||||
attachmentView,
|
|
||||||
existing.Width,
|
|
||||||
existing.Height);
|
|
||||||
existing.RenderPass = promotedRenderPass;
|
|
||||||
existing.Framebuffer = promotedFramebuffer;
|
|
||||||
var promotedName = GuestImageDebugName(target, format);
|
|
||||||
SetDebugName(ObjectType.RenderPass, promotedRenderPass.Handle, $"{promotedName} renderpass");
|
|
||||||
SetDebugName(ObjectType.Framebuffer, promotedFramebuffer.Handle, $"{promotedName} framebuffer");
|
|
||||||
}
|
|
||||||
|
|
||||||
return existing;
|
return existing;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4520,48 +4171,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
mipViews[mipLevel] = mipView;
|
mipViews[mipLevel] = mipView;
|
||||||
}
|
}
|
||||||
|
|
||||||
var (renderPass, framebuffer) = CreateRenderPassAndFramebuffer(
|
|
||||||
format,
|
|
||||||
mipViews[0],
|
|
||||||
target.Width,
|
|
||||||
target.Height);
|
|
||||||
|
|
||||||
var resource = new GuestImageResource
|
|
||||||
{
|
|
||||||
Address = target.Address,
|
|
||||||
Width = target.Width,
|
|
||||||
Height = target.Height,
|
|
||||||
MipLevels = mipLevels,
|
|
||||||
Format = format,
|
|
||||||
Image = image,
|
|
||||||
Memory = memory,
|
|
||||||
View = view,
|
|
||||||
MipViews = mipViews,
|
|
||||||
RenderPass = renderPass,
|
|
||||||
Framebuffer = framebuffer,
|
|
||||||
};
|
|
||||||
var debugName = GuestImageDebugName(target, format);
|
|
||||||
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
|
|
||||||
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
|
|
||||||
for (var mipLevel = 0; mipLevel < mipViews.Length; mipLevel++)
|
|
||||||
{
|
|
||||||
SetDebugName(
|
|
||||||
ObjectType.ImageView,
|
|
||||||
mipViews[mipLevel].Handle,
|
|
||||||
$"{debugName} mip{mipLevel}");
|
|
||||||
}
|
|
||||||
SetDebugName(ObjectType.RenderPass, renderPass.Handle, $"{debugName} renderpass");
|
|
||||||
SetDebugName(ObjectType.Framebuffer, framebuffer.Handle, $"{debugName} framebuffer");
|
|
||||||
_guestImages.Add(target.Address, resource);
|
|
||||||
return resource;
|
|
||||||
}
|
|
||||||
|
|
||||||
private (RenderPass RenderPass, Framebuffer Framebuffer) CreateRenderPassAndFramebuffer(
|
|
||||||
Format format,
|
|
||||||
ImageView attachmentView,
|
|
||||||
uint width,
|
|
||||||
uint height)
|
|
||||||
{
|
|
||||||
var colorAttachment = new AttachmentDescription
|
var colorAttachment = new AttachmentDescription
|
||||||
{
|
{
|
||||||
Format = format,
|
Format = format,
|
||||||
@@ -4596,22 +4205,49 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_vk.CreateRenderPass(_device, &renderPassInfo, null, out var renderPass),
|
_vk.CreateRenderPass(_device, &renderPassInfo, null, out var renderPass),
|
||||||
"vkCreateRenderPass(offscreen)");
|
"vkCreateRenderPass(offscreen)");
|
||||||
|
|
||||||
var attachment = attachmentView;
|
var attachment = mipViews[0];
|
||||||
var framebufferInfo = new FramebufferCreateInfo
|
var framebufferInfo = new FramebufferCreateInfo
|
||||||
{
|
{
|
||||||
SType = StructureType.FramebufferCreateInfo,
|
SType = StructureType.FramebufferCreateInfo,
|
||||||
RenderPass = renderPass,
|
RenderPass = renderPass,
|
||||||
AttachmentCount = 1,
|
AttachmentCount = 1,
|
||||||
PAttachments = &attachment,
|
PAttachments = &attachment,
|
||||||
Width = width,
|
Width = target.Width,
|
||||||
Height = height,
|
Height = target.Height,
|
||||||
Layers = 1,
|
Layers = 1,
|
||||||
};
|
};
|
||||||
Check(
|
Check(
|
||||||
_vk.CreateFramebuffer(_device, &framebufferInfo, null, out var framebuffer),
|
_vk.CreateFramebuffer(_device, &framebufferInfo, null, out var framebuffer),
|
||||||
"vkCreateFramebuffer(offscreen)");
|
"vkCreateFramebuffer(offscreen)");
|
||||||
|
|
||||||
return (renderPass, framebuffer);
|
var resource = new GuestImageResource
|
||||||
|
{
|
||||||
|
Address = target.Address,
|
||||||
|
Width = target.Width,
|
||||||
|
Height = target.Height,
|
||||||
|
MipLevels = mipLevels,
|
||||||
|
Format = format,
|
||||||
|
Image = image,
|
||||||
|
Memory = memory,
|
||||||
|
View = view,
|
||||||
|
MipViews = mipViews,
|
||||||
|
RenderPass = renderPass,
|
||||||
|
Framebuffer = framebuffer,
|
||||||
|
};
|
||||||
|
var debugName = GuestImageDebugName(target, format);
|
||||||
|
SetDebugName(ObjectType.Image, image.Handle, $"{debugName} image");
|
||||||
|
SetDebugName(ObjectType.ImageView, view.Handle, $"{debugName} view");
|
||||||
|
for (var mipLevel = 0; mipLevel < mipViews.Length; mipLevel++)
|
||||||
|
{
|
||||||
|
SetDebugName(
|
||||||
|
ObjectType.ImageView,
|
||||||
|
mipViews[mipLevel].Handle,
|
||||||
|
$"{debugName} mip{mipLevel}");
|
||||||
|
}
|
||||||
|
SetDebugName(ObjectType.RenderPass, renderPass.Handle, $"{debugName} renderpass");
|
||||||
|
SetDebugName(ObjectType.Framebuffer, framebuffer.Handle, $"{debugName} framebuffer");
|
||||||
|
_guestImages.Add(target.Address, resource);
|
||||||
|
return resource;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint ClampMipLevels(uint width, uint height, uint requestedMipLevels)
|
private static uint ClampMipLevels(uint width, uint height, uint requestedMipLevels)
|
||||||
@@ -4833,10 +4469,7 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
|
|
||||||
_commandBuffer = _presentationCommandBuffer;
|
_commandBuffer = _presentationCommandBuffer;
|
||||||
if (!_deviceLost)
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||||
{
|
|
||||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
|
||||||
}
|
|
||||||
|
|
||||||
var completedWork = 0;
|
var completedWork = 0;
|
||||||
while (completedWork < MaxGuestWorkPerRender &&
|
while (completedWork < MaxGuestWorkPerRender &&
|
||||||
@@ -5694,12 +5327,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
|
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ShouldTraceVulkanResources() =>
|
|
||||||
string.Equals(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VK_RESOURCES"),
|
|
||||||
"1",
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
|
|
||||||
private void RecordTranslatedGraphicsPass(
|
private void RecordTranslatedGraphicsPass(
|
||||||
TranslatedDrawResources resources,
|
TranslatedDrawResources resources,
|
||||||
RenderPass renderPass,
|
RenderPass renderPass,
|
||||||
@@ -6329,10 +5956,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (_debugUtils is not null && _debugMessenger.Handle != 0)
|
|
||||||
{
|
|
||||||
_debugUtils.DestroyDebugUtilsMessenger(_instance, _debugMessenger, null);
|
|
||||||
}
|
|
||||||
_vulkanReady = false;
|
_vulkanReady = false;
|
||||||
_vk.DeviceWaitIdle(_device);
|
_vk.DeviceWaitIdle(_device);
|
||||||
CollectCompletedGuestSubmissions(waitForOldest: false);
|
CollectCompletedGuestSubmissions(waitForOldest: false);
|
||||||
@@ -6535,25 +6158,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryMarkDeviceLost(Exception exception)
|
|
||||||
{
|
|
||||||
if (!exception.Message.Contains(nameof(Result.ErrorDeviceLost), StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_deviceLost = true;
|
|
||||||
if (!_deviceLostLogged)
|
|
||||||
{
|
|
||||||
_deviceLostLogged = true;
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
"[LOADER][ERROR] Vulkan device lost; dropping subsequent guest GPU work. " +
|
|
||||||
exception.Message);
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceVulkanShader(string message)
|
private static void TraceVulkanShader(string message)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) &&
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal) &&
|
||||||
|
|||||||
@@ -11,16 +11,6 @@
|
|||||||
"Silk.NET.Core": "2.23.0"
|
"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": {
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
|
|||||||
@@ -4,7 +4,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
-->
|
-->
|
||||||
|
|
||||||
<Project Sdk="Microsoft.NET.Sdk">
|
<Project Sdk="Microsoft.NET.Sdk">
|
||||||
<PropertyGroup>
|
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
Reference in New Issue
Block a user