mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
28 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9a828f8ed3 | |||
| a63a7de35a | |||
| fe1a592496 | |||
| 9d4dad2c24 | |||
| 38d94d8e54 | |||
| 61a1abdb13 | |||
| 88452218cb | |||
| 427a511e48 | |||
| e103795768 | |||
| 24c6e44800 | |||
| 3a30d6419f | |||
| 2d8a795f23 | |||
| adb2acdb39 | |||
| 5e5bead02c | |||
| e98f02966a | |||
| 9ab1e2f21a | |||
| 340eb9e0ab | |||
| 03e3d25e85 | |||
| 2bc466b10e | |||
| 45d0be474b | |||
| 5bb91eff9b | |||
| f4df0ed4bd | |||
| 23691a2bdc | |||
| 5c5ed8c064 | |||
| a4748ce266 | |||
| 5617646c8a | |||
| a4f3d3cd7f | |||
| 92526ecacf |
@@ -16,11 +16,6 @@ body:
|
||||
Example title:
|
||||
Demon’s Souls (PPSA01341)
|
||||
|
||||
**Before reporting:**
|
||||
- Test the game on a **fresh build from `main`**, not an old release.
|
||||
- Copy the **exact commit SHA** shown at the top of the log.
|
||||
- `DEBUG` and `TRACE` lines are diagnostic context, not errors by themselves; report the last milestone or the first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop.
|
||||
|
||||
- type: input
|
||||
id: game_name
|
||||
attributes:
|
||||
@@ -78,20 +73,11 @@ body:
|
||||
id: logs
|
||||
attributes:
|
||||
label: Log File
|
||||
description: Attach the **full** log file (`.log`, `.txt`, or `.zip`). Do not paste only a fragment — the maintainer needs the complete log from launch to crash.
|
||||
description: Drag and drop your log file here, or click to browse
|
||||
validations:
|
||||
required: true
|
||||
required: false
|
||||
accept: ".log,.txt,.zip"
|
||||
|
||||
- type: input
|
||||
id: last_milestone_first_error
|
||||
attributes:
|
||||
label: Last Milestone / First Error
|
||||
description: Paste the **last useful milestone line** or the **first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop** from the log. `DEBUG`/`TRACE` lines do not count unless a real error follows them.
|
||||
placeholder: "e.g. CpuEngine: native-only OR [ERROR] Native backend FAILED: ..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: operating_system
|
||||
attributes:
|
||||
@@ -133,8 +119,7 @@ body:
|
||||
id: emulator_version
|
||||
attributes:
|
||||
label: Emulator Version / Commit
|
||||
description: Copy the **exact short SHA** from the top of the log. Do not enter just `0.0.1` without a commit hash.
|
||||
placeholder: e.g. a1b2c3d
|
||||
placeholder: e.g. v0.0.1 / a1b2c3d
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 142 KiB |
@@ -37,6 +37,3 @@ packages/
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
ehthumbs.db
|
||||
|
||||
.vs/
|
||||
.idea/
|
||||
|
||||
@@ -32,18 +32,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Build provenance surfaced by SharpEmu.Logging.BuildInfo as a banner at the
|
||||
top of the log. Populated from GitHub Actions environment variables when
|
||||
building in CI; empty locally. -->
|
||||
<ItemGroup>
|
||||
<AssemblyMetadata Include="SharpEmu.BuildConfiguration" Value="$(Configuration)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_SHA)' != ''" Include="SharpEmu.BuildSha" Value="$(GITHUB_SHA)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_REF_NAME)' != ''" Include="SharpEmu.BuildBranch" Value="$(GITHUB_REF_NAME)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_REF)' != ''" Include="SharpEmu.BuildRef" Value="$(GITHUB_REF)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_EVENT_NAME)' != ''" Include="SharpEmu.BuildEventName" Value="$(GITHUB_EVENT_NAME)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_REPOSITORY)' != ''" Include="SharpEmu.BuildRepository" Value="$(GITHUB_REPOSITORY)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_SERVER_URL)' != ''" Include="SharpEmu.BuildServerUrl" Value="$(GITHUB_SERVER_URL)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_RUN_ID)' != ''" Include="SharpEmu.BuildRunId" Value="$(GITHUB_RUN_ID)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
@@ -7,16 +7,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Alex Barney
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -15,9 +15,3 @@ path = [
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||
SPDX-License-Identifier = "GPL-2.0-or-later"
|
||||
|
||||
[[annotations]]
|
||||
path = "src/SharpEmu.GUI/Atrac9/**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2018 Alex Barney"
|
||||
SPDX-License-Identifier = "MIT"
|
||||
|
||||
@@ -7,7 +7,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
|
||||
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
|
||||
<Project Path="src/SharpEmu.GUI/SharpEmu.GUI.csproj" />
|
||||
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
|
||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||
|
||||
+2
-160
@@ -3,7 +3,6 @@
|
||||
|
||||
using SharpEmu.Core.Runtime;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.GUI;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
using System.Runtime.InteropServices;
|
||||
@@ -25,45 +24,12 @@ internal static partial class Program
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
private const int ATTACH_PARENT_PROCESS = -1;
|
||||
private const int STD_OUTPUT_HANDLE = -11;
|
||||
private const int STD_ERROR_HANDLE = -12;
|
||||
private const uint GENERIC_READ = 0x80000000;
|
||||
private const uint GENERIC_WRITE = 0x40000000;
|
||||
private const uint FILE_SHARE_READ = 0x00000001;
|
||||
private const uint FILE_SHARE_WRITE = 0x00000002;
|
||||
private const uint OPEN_EXISTING = 3;
|
||||
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Run(args);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SharpEmuLog.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private static int Run(string[] args)
|
||||
{
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
if (args.Length == 0 && !isMitigatedChild)
|
||||
{
|
||||
// No arguments: open the desktop frontend. Any argument selects
|
||||
// the classic CLI behavior below.
|
||||
return GuiLauncher.Run();
|
||||
}
|
||||
|
||||
// The executable uses the GUI subsystem, so CLI mode has to connect
|
||||
// itself to a console before the first write.
|
||||
EnsureCliConsole();
|
||||
UseUtf8ConsoleOutput();
|
||||
|
||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
||||
{
|
||||
return childExitCode;
|
||||
@@ -77,11 +43,9 @@ internal static partial class Program
|
||||
|
||||
SharpEmuLog.MinimumLevel = logLevel;
|
||||
|
||||
Log.Info(BuildInfo.Banner);
|
||||
|
||||
ebootPath = Path.GetFullPath(ebootPath);
|
||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||
|
||||
|
||||
if (!File.Exists(ebootPath))
|
||||
{
|
||||
Log.Error($"EBOOT file was not found: {ebootPath}");
|
||||
@@ -137,100 +101,6 @@ internal static partial class Program
|
||||
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
|
||||
}
|
||||
|
||||
private static void EnsureCliConsole()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard handles already provided (pipes or file redirection, e.g.
|
||||
// when the GUI or a script launches us): use them as-is.
|
||||
if (IsHandleValid(GetStdHandle(STD_OUTPUT_HANDLE)) && IsHandleValid(GetStdHandle(STD_ERROR_HANDLE)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer the console of the parent process (interactive terminal);
|
||||
// create one only when started with arguments but no terminal at all
|
||||
// (e.g. a shortcut), so usage and errors remain visible.
|
||||
if (!AttachConsole(ATTACH_PARENT_PROCESS) && GetConsoleWindow() == 0)
|
||||
{
|
||||
_ = AllocConsole();
|
||||
}
|
||||
|
||||
RebindStdHandleToConsole(STD_OUTPUT_HANDLE);
|
||||
RebindStdHandleToConsole(STD_ERROR_HANDLE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes console writes UTF-8 so the GUI's pipe reader (and any modern
|
||||
/// terminal) decodes non-ASCII text correctly. Without this, redirected
|
||||
/// output falls back to the OS ANSI code page and characters like "—"
|
||||
/// arrive mangled.
|
||||
/// </summary>
|
||||
private static void UseUtf8ConsoleOutput()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Also recreates the redirected Console.Out/Error writers with
|
||||
// the new encoding.
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// No attached console (GUI-subsystem child with piped output):
|
||||
// wrap the raw handles instead.
|
||||
}
|
||||
|
||||
if (Console.IsOutputRedirected)
|
||||
{
|
||||
Console.SetOut(new StreamWriter(
|
||||
Console.OpenStandardOutput(),
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
|
||||
{
|
||||
AutoFlush = true,
|
||||
});
|
||||
}
|
||||
|
||||
if (Console.IsErrorRedirected)
|
||||
{
|
||||
Console.SetError(new StreamWriter(
|
||||
Console.OpenStandardError(),
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
|
||||
{
|
||||
AutoFlush = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void RebindStdHandleToConsole(int stdHandle)
|
||||
{
|
||||
if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var conOut = CreateFileW(
|
||||
"CONOUT$",
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
0,
|
||||
OPEN_EXISTING,
|
||||
0,
|
||||
0);
|
||||
if (IsHandleValid(conOut))
|
||||
{
|
||||
_ = SetStdHandle(stdHandle, conOut);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsHandleValid(nint handle)
|
||||
{
|
||||
return handle != 0 && handle != -1;
|
||||
}
|
||||
|
||||
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
||||
{
|
||||
isMitigatedChild = false;
|
||||
@@ -800,32 +670,4 @@ internal static partial class Program
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(nint handle);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern nint GetConsoleWindow();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AttachConsole(int processId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AllocConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint GetStdHandle(int stdHandle);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetStdHandle(int stdHandle, nint handle);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern nint CreateFileW(
|
||||
string fileName,
|
||||
uint desiredAccess,
|
||||
uint shareMode,
|
||||
nint securityAttributes,
|
||||
uint creationDisposition,
|
||||
uint flagsAndAttributes,
|
||||
nint templateFile);
|
||||
}
|
||||
|
||||
@@ -7,19 +7,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- GUI subsystem so starting without arguments opens the frontend with no
|
||||
console window; CLI mode re-attaches to the parent terminal's console. -->
|
||||
<OutputType>WinExe</OutputType>
|
||||
<OutputType>Exe</OutputType>
|
||||
<AssemblyName>SharpEmu</AssemblyName>
|
||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
|
||||
<SelfContained>true</SelfContained>
|
||||
<PublishSingleFile>true</PublishSingleFile>
|
||||
<IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract>
|
||||
<!-- <IncludeNativeLibrariesForSelfExtract>true</IncludeNativeLibrariesForSelfExtract> -->
|
||||
<EnableCompressionInSingleFile>true</EnableCompressionInSingleFile>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
@@ -38,10 +35,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
@@ -50,17 +43,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||
library is embedded into the single-file bundle. -->
|
||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
||||
<ItemGroup>
|
||||
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
|
||||
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw'))" />
|
||||
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
|
||||
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,105 +8,6 @@
|
||||
"resolved": "10.0.3",
|
||||
"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": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
@@ -158,38 +59,6 @@
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"SkiaSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||
"dependencies": {
|
||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
@@ -204,17 +73,6 @@
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.gui": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Avalonia": "[11.3.18, )",
|
||||
"Avalonia.Desktop": "[11.3.18, )",
|
||||
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
||||
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
||||
"SharpEmu.Logging": "[1.0.0, )",
|
||||
"Tmds.DBus.Protocol": "[0.21.3, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
},
|
||||
@@ -231,48 +89,6 @@
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Avalonia": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "11.3.18",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Native": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18",
|
||||
"Avalonia.Win32": "11.3.18",
|
||||
"Avalonia.X11": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Iced": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[1.21.0, )",
|
||||
@@ -317,61 +133,9 @@
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[0.21.3, )",
|
||||
"resolved": "0.21.3",
|
||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||
}
|
||||
},
|
||||
"net10.0/linux-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
@@ -379,52 +143,6 @@
|
||||
}
|
||||
},
|
||||
"net10.0/osx-arm64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
@@ -432,52 +150,6 @@
|
||||
}
|
||||
},
|
||||
"net10.0/win-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
|
||||
@@ -7,14 +7,11 @@ using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu;
|
||||
|
||||
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
|
||||
|
||||
private enum EntryFrameKind
|
||||
{
|
||||
ProcessEntry,
|
||||
@@ -83,8 +80,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string processImageName = "eboot.bin",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Log.Debug("=== DispatchEntry START ===");
|
||||
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -92,7 +89,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -105,8 +103,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string moduleName = "module",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Log.Debug("=== DispatchModuleInitializer START ===");
|
||||
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -121,7 +119,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -135,7 +134,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
CpuExecutionOptions executionOptions = default,
|
||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||
{
|
||||
Log.Debug("DispatchEntryCore STARTING...");
|
||||
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
||||
|
||||
LastEntryPoint = entryPoint;
|
||||
LastTrapInfo = null;
|
||||
@@ -307,7 +306,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
LastMilestoneLog,
|
||||
Environment.NewLine,
|
||||
$"CpuEngine native-only failed: {backendError}");
|
||||
Log.Error($"Native backend FAILED: {backendError}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
||||
return FailEarly(
|
||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||
CpuExitReason.NativeBackendUnavailable);
|
||||
@@ -416,8 +415,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
|
||||
const ulong entryParamsSize = 0x20;
|
||||
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
||||
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
|
||||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
|
||||
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
|
||||
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
||||
@@ -457,6 +456,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
return value & ~(alignment - 1);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return context.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static string BuildEntryFrameDiagnostic(
|
||||
ulong entryPoint,
|
||||
CpuContext context,
|
||||
|
||||
@@ -8,7 +8,6 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
@@ -44,7 +43,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return;
|
||||
}
|
||||
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Recent import calls ({_recentImportTraceCount}):");
|
||||
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
|
||||
for (int i = 0; i < _recentImportTraceCount; i++)
|
||||
{
|
||||
@@ -52,8 +51,8 @@ public sealed partial class DirectExecutionBackend
|
||||
var entry = _recentImportTrace[num2];
|
||||
if (!string.IsNullOrEmpty(entry.Nid))
|
||||
{
|
||||
Log.Info(
|
||||
$" #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
|
||||
}
|
||||
}
|
||||
@@ -92,12 +91,12 @@ public sealed partial class DirectExecutionBackend
|
||||
list.Add(num7);
|
||||
if (num2 < 32)
|
||||
{
|
||||
Log.Info($"Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
|
||||
num2++;
|
||||
}
|
||||
if (num >= 16384)
|
||||
{
|
||||
Log.Warning($"Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
|
||||
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -107,7 +106,7 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
if (num != 0)
|
||||
{
|
||||
Log.Warning($"Suspicious unresolved pointer hits: {num}");
|
||||
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer hits: {num}");
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -122,18 +121,18 @@ public sealed partial class DirectExecutionBackend
|
||||
Span<byte> destination = stackalloc byte[128];
|
||||
if (!cpuContext.Memory.TryRead(returnRip, destination))
|
||||
{
|
||||
Log.Debug($"Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
|
||||
return;
|
||||
}
|
||||
string value = BitConverter.ToString(destination.ToArray()).Replace("-", " ");
|
||||
Log.Debug($"Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
|
||||
if (destination[0] == byte.MaxValue && (destination[1] == 21 || destination[1] == 37))
|
||||
{
|
||||
int num = BitConverter.ToInt32(destination.Slice(2, 4));
|
||||
ulong num2 = returnRip + 6 + (ulong)num;
|
||||
if (cpuContext.TryReadUInt64(num2, out var value2))
|
||||
{
|
||||
Log.Debug($"Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
|
||||
}
|
||||
}
|
||||
if (destination[0] == 72 && destination[1] == 139 && destination[2] == 5)
|
||||
@@ -142,7 +141,7 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong num4 = returnRip + 7 + (ulong)num3;
|
||||
if (cpuContext.TryReadUInt64(num4, out var value3))
|
||||
{
|
||||
Log.Debug($"Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
|
||||
}
|
||||
}
|
||||
for (int i = 0; i + 6 <= destination.Length; i++)
|
||||
@@ -154,7 +153,7 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong num7 = num6 + 6 + (ulong)num5;
|
||||
if (cpuContext.TryReadUInt64(num7, out var value4))
|
||||
{
|
||||
Log.Debug($"Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -169,7 +168,7 @@ public sealed partial class DirectExecutionBackend
|
||||
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
|
||||
ulong callRip = returnRip + (ulong)i;
|
||||
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
||||
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||
{
|
||||
if (_importEntries[importIndex].Address != target)
|
||||
@@ -180,21 +179,21 @@ public sealed partial class DirectExecutionBackend
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (cpuContext.Memory.TryRead(target, targetBytes))
|
||||
{
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
||||
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
|
||||
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
|
||||
{
|
||||
@@ -202,8 +201,8 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
|
||||
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
|
||||
{
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||
{
|
||||
if (_importEntries[importIndex].Address != slotTarget)
|
||||
@@ -214,13 +213,13 @@ public sealed partial class DirectExecutionBackend
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -103,15 +103,6 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||
|
||||
// Thread-mode probe: a hardware exception raised while this thread is inside
|
||||
// the managed import gateway means the VEH->managed reentry happened from
|
||||
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
|
||||
if (LogThreadMode && _threadModeGatewayDepth > 0)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
||||
}
|
||||
|
||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
{
|
||||
return -1;
|
||||
|
||||
@@ -19,16 +19,6 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeGatewayCalls++;
|
||||
_threadModeGatewayDepth++;
|
||||
if (!_threadModeGatewayFirstLogged)
|
||||
{
|
||||
_threadModeGatewayFirstLogged = true;
|
||||
TraceThreadMode($"gateway_first import={importIndex} total={_threadModeGatewayCalls}");
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
|
||||
@@ -46,13 +36,6 @@ public sealed partial class DirectExecutionBackend
|
||||
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
|
||||
return 18446744071562199298uL;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeGatewayDepth--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
|
||||
@@ -86,12 +69,6 @@ public sealed partial class DirectExecutionBackend
|
||||
WriteCtxU64(contextRecord, 152, nextRsp);
|
||||
WriteCtxU64(contextRecord, 248, returnRip);
|
||||
Interlocked.Increment(ref _rawSentinelRecoveries);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"sentinel_recover rip=0x{value:X16} -> 0x{returnRip:X16} rsp=0x{rsp:X16} -> 0x{nextRsp:X16} " +
|
||||
$"gateway_depth={_threadModeGatewayDepth}");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -122,12 +99,6 @@ public sealed partial class DirectExecutionBackend
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
|
||||
_lastReportedRawSentinelRecoveries = num2;
|
||||
}
|
||||
// Leaf imports take a scalar-only fast path that reads its own operands and
|
||||
// intentionally bypasses the SysV variadic XMM marshalling below. This is safe
|
||||
// only while the leaf set contains no float-variadic or float-returning function.
|
||||
// The constraint and the audited list live on IsLeafImport — do not add a
|
||||
// function that consumes xmm0-7 args or returns in xmm0 to the leaf set;
|
||||
// such functions must stay on the full gateway path below.
|
||||
if (IsLeafImport(importStubEntry.Nid) &&
|
||||
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
|
||||
{
|
||||
@@ -147,17 +118,10 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
|
||||
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
||||
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
||||
// The trampoline spills the SysV variadic XMM save area (xmm0..xmm7) into the
|
||||
// 0x80 bytes immediately below the GP argpack, so variadic float args (printf
|
||||
// %f, and powf/logf inputs) reach the handler. xmm{i} is at argPackPtr-0x80+i*16.
|
||||
for (var xmmIndex = 0; xmmIndex < 8; xmmIndex++)
|
||||
{
|
||||
var xmmSlot = argPackPtr - 0x80 + (xmmIndex * 16);
|
||||
cpuContext.SetXmmRegister(
|
||||
xmmIndex,
|
||||
*(ulong*)xmmSlot,
|
||||
*(ulong*)(xmmSlot + 8));
|
||||
}
|
||||
cpuContext.SetXmmRegister(
|
||||
0,
|
||||
*(ulong*)(argPackPtr - 16),
|
||||
*(ulong*)(argPackPtr - 8));
|
||||
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
||||
ulong value = cpuContext[CpuRegister.Rdi];
|
||||
ulong value2 = cpuContext[CpuRegister.Rsi];
|
||||
@@ -227,17 +191,6 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rax] = 1uL;
|
||||
return 1uL;
|
||||
}
|
||||
// Backend teardown in progress: unwind this worker to the host instead of
|
||||
// dispatching. RunGuestThread parks it as Blocked with no wake handler, its
|
||||
// host thread exits, and RequestGuestThreadTeardown can finish before any
|
||||
// executable memory is freed.
|
||||
if (isGuestWorker &&
|
||||
_guestTeardownRequested &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "backend teardown"))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 0uL;
|
||||
return 0uL;
|
||||
}
|
||||
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
||||
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
||||
@@ -405,21 +358,12 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
orbisGen2Result = (OrbisGen2Result)returnValue;
|
||||
}
|
||||
else if (importStubEntry.Export is { } mismatchedExport)
|
||||
{
|
||||
dispatchResolved = true;
|
||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Import#{num} not implemented for generation {cpuContext.TargetGeneration}: " +
|
||||
$"nid={importStubEntry.Nid} targets={mismatchedExport.Target} ret=0x{num7:X16}");
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchResolved = false;
|
||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchResolved = false;
|
||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -538,13 +482,6 @@ public sealed partial class DirectExecutionBackend
|
||||
Console.Error.Flush();
|
||||
}
|
||||
}
|
||||
// Publish the handler's XMM0 back into the argpack's xmm0 save slot; the
|
||||
// trampoline epilogue reloads it into the guest's XMM0, delivering float/double
|
||||
// return values (powf/logf/wcstod). Harmless for int/pointer returns (XMM is
|
||||
// volatile across a SysV call, so the guest never relies on a preserved XMM0).
|
||||
cpuContext.GetXmmRegister(0, out var returnXmm0Low, out var returnXmm0High);
|
||||
*(ulong*)(argPackPtr - 0x80) = returnXmm0Low;
|
||||
*(ulong*)(argPackPtr - 0x80 + 8) = returnXmm0High;
|
||||
return cpuContext[CpuRegister.Rax];
|
||||
}
|
||||
catch (Exception ex)
|
||||
@@ -676,13 +613,6 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
// Subset of the leaf set that additionally skips the import-call-frame
|
||||
// bookkeeping. The same scalar-only (no XMM args, no XMM return) constraint
|
||||
// as IsLeafImport applies — see the note there before adding entries.
|
||||
// NOTE: this filter is only consulted after IsLeafImport accepts the NID;
|
||||
// entries listed here but not in IsLeafImport (scePadRead, scePadOpen,
|
||||
// sceUserServiceGetEvent, sceUserServiceGetPlatformPrivacySetting,
|
||||
// pthread_mutex_trylock) currently take the full gateway path.
|
||||
private static bool IsNoBlockLeafImport(string nid) =>
|
||||
nid is
|
||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||
@@ -709,7 +639,7 @@ public sealed partial class DirectExecutionBackend
|
||||
"xk0AcarP3V4" or // scePadOpen
|
||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
||||
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
|
||||
"K-jXhbt2gn4"; // pthread_mutex_trylock (scePthreadMutexTrylock is upoVrzMHFeE)
|
||||
"K-jXhbt2gn4"; // scePthreadMutexTrylock
|
||||
|
||||
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
||||
{
|
||||
@@ -776,41 +706,22 @@ public sealed partial class DirectExecutionBackend
|
||||
"1G3lF1Gg1k8" or // sceKernelOpen
|
||||
"gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes
|
||||
|
||||
// LEAF-IMPORT REGISTRATION — scalar-only constraint.
|
||||
//
|
||||
// TryDispatchLeafImport is a fast path that never copies the trampoline's XMM
|
||||
// save area into CpuContext and never publishes an XMM0 return back to the
|
||||
// guest (see DispatchImport). Every NID listed here must therefore satisfy BOTH:
|
||||
// 1. no float/double parameters (nothing passed in xmm0..xmm7), and
|
||||
// 2. no float/double return value (nothing returned in xmm0).
|
||||
// Integer/pointer arguments and returns only. va_list-based functions
|
||||
// (vsnprintf) are safe: SysV va_list floats are read from the caller-built
|
||||
// reg_save_area in guest memory, not from the callee's incoming XMM registers.
|
||||
//
|
||||
// If a function that consumes XMM arguments or returns in xmm0 (e.g. powf,
|
||||
// logf, wcstod, sceVideoOutColorSettingsSetGamma_) is ever added here, its
|
||||
// float arguments will silently arrive stale and its return value will be
|
||||
// dropped. Such functions must stay on the full gateway path — do not list them.
|
||||
//
|
||||
// Audited 2026-07-11 (PR #59): every NID below maps to a registered
|
||||
// SysAbiExport whose handler neither reads nor writes CpuContext XMM state
|
||||
// and whose known guest signature is integer/pointer only.
|
||||
private bool IsLeafImport(string nid)
|
||||
{
|
||||
if (nid == "1jfXLRVzisc") // sceKernelUsleep
|
||||
if (nid == "1jfXLRVzisc")
|
||||
{
|
||||
return !_logUsleep;
|
||||
}
|
||||
|
||||
return nid is
|
||||
"9UK1vLZQft4" or // scePthreadMutexLock
|
||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||
"7H0iTOciTLo" or // pthread_mutex_lock
|
||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||
"9UK1vLZQft4" or
|
||||
"tn3VlD0hG60" or
|
||||
"7H0iTOciTLo" or
|
||||
"2Z+PpY6CaJg" or
|
||||
"8aI7R7WaOlc" or
|
||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||
"qj7QZpgr9Uw" or // sceAgcUnknownQj7QZpgr9Uw (unknown NID; observed scalar: command-buffer pointer in, packet address out)
|
||||
"qj7QZpgr9Uw" or // Gen5 graphics type-2 packet
|
||||
"LtTouSCZjHM" or // sceAgcCbNop
|
||||
"k3GhuSNmBLU" or // sceAgcCbDispatch
|
||||
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
|
||||
@@ -833,24 +744,24 @@ public sealed partial class DirectExecutionBackend
|
||||
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
|
||||
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
|
||||
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
|
||||
"a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
|
||||
"Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
|
||||
"GuchCTefuZw" or // sceAmprCommandBufferDestructor
|
||||
"N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
|
||||
"baQO9ez2gL4" or // sceAmprCommandBufferReset
|
||||
"ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
|
||||
"mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
|
||||
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
|
||||
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
|
||||
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
|
||||
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
|
||||
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
|
||||
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
|
||||
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
|
||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||
"a8uLzYY--tM" or
|
||||
"Qs1xtplKo0U" or
|
||||
"GuchCTefuZw" or
|
||||
"N-FSPA4S3nI" or
|
||||
"baQO9ez2gL4" or
|
||||
"ULvXMDz56po" or
|
||||
"mQ16-QdKv7k" or
|
||||
"vWU-odnS+fU" or
|
||||
"sSAUCCU1dv4" or
|
||||
"C+IEj+BsAFM" or
|
||||
"tZDDEo2tE5k" or
|
||||
"GnxKOHEawhk" or
|
||||
"H896Pt-yB4I" or
|
||||
"sJXyWHjP-F8" or
|
||||
"ASoW5WE-UPo" or
|
||||
"rqwFKI4PAiM" or
|
||||
"eE4Szl8sil8" or
|
||||
"qvMUCyyaCSI" or
|
||||
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
|
||||
"TywrFKCoLGY" or // sceSaveDataInitialize3
|
||||
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
|
||||
@@ -894,12 +805,7 @@ public sealed partial class DirectExecutionBackend
|
||||
"vz+pg2zdopI" or // sceKernelGetEventUserData
|
||||
"mJ7aghmgvfc" or // sceKernelGetEventId
|
||||
"23CPPI1tyBY" or // sceKernelGetEventFilter
|
||||
"kwGyyjohI50" or // sceKernelGetEventData
|
||||
// _Getpctype is a per-character ctype table lookup; the embedded mcpp preprocessor
|
||||
// re-fetches the table pointer on every isdigit()/isalpha() check, so classifying a
|
||||
// large input legitimately calls it hundreds of thousands of times back-to-back -
|
||||
// real, finite work that would otherwise trip the loop guard.
|
||||
"sUP1hBaouOw"; // _Getpctype
|
||||
"kwGyyjohI50"; // sceKernelGetEventData
|
||||
}
|
||||
|
||||
private long NextImportDispatchIndex()
|
||||
|
||||
@@ -1,582 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
// Guest entry stubs must not run above CLR-managed frames on a CLR-created thread:
|
||||
// the suspension/stack-walk machinery then has to traverse a frame chain that is
|
||||
// interleaved with guest stubs carrying no CLR unwind info, and any window in which
|
||||
// the thread-mode bookkeeping disagrees with the actual stack (import dispatch, VEH
|
||||
// redirection) fail-fasts the runtime — the audio_output_thread ~7th-cycle
|
||||
// "attempted to call a UnmanagedCallersOnly method from managed code" crash.
|
||||
//
|
||||
// Guest execution therefore runs on dedicated raw OS threads whose run loop is
|
||||
// emitted native code. While guest code executes there is not a single managed
|
||||
// frame on the thread and it sits in preemptive mode, so the GC ignores it
|
||||
// entirely. The only managed activity on the thread is the reverse-P/Invoke import
|
||||
// dispatch and the per-run prologue/epilogue, which the CLR supports on foreign
|
||||
// threads (lazy attach, preemptive between calls). The managed orchestrator
|
||||
// (RunGuestThread / the main execution thread) parks in a preemptive wait for the
|
||||
// duration of the run, so its own frame chain is never walked across guest frames.
|
||||
private static readonly bool NativeGuestWorkersDisabled =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS"), "1", StringComparison.Ordinal);
|
||||
|
||||
private readonly object _nativeWorkerGate = new();
|
||||
private readonly List<NativeGuestExecutor> _allNativeWorkers = new();
|
||||
private readonly Stack<NativeGuestExecutor> _idleNativeWorkers = new();
|
||||
private bool _nativeWorkersDisposed;
|
||||
private int _nativeWorkerCreationFailedLogged;
|
||||
|
||||
private const uint StackSizeParamIsAReservation = 0x00010000u;
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint CreateThread(
|
||||
nint lpThreadAttributes,
|
||||
nuint dwStackSize,
|
||||
nint lpStartAddress,
|
||||
nint lpParameter,
|
||||
uint dwCreationFlags,
|
||||
out uint lpThreadId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint WaitForSingleObject(nint hHandle, uint dwMilliseconds);
|
||||
|
||||
// Runs an emitted guest entry stub. Preferred path is a pooled native worker
|
||||
// thread; falls back to the historical inline calli (guest frames above this
|
||||
// thread's managed frames) when workers are disabled or unavailable.
|
||||
//
|
||||
// Callers set the Active* thread-statics before emitting the stub and read the
|
||||
// yield/forced-exit flags right after this returns, so the worker outcome is
|
||||
// copied back into this thread's statics before returning.
|
||||
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot)
|
||||
{
|
||||
var worker = RentNativeGuestExecutor();
|
||||
if (worker is null)
|
||||
{
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
return CallNativeEntry(entryStub);
|
||||
}
|
||||
try
|
||||
{
|
||||
var state = _activeGuestThreadState;
|
||||
var nativeReturn = worker.Run(
|
||||
_activeCpuContext!,
|
||||
state,
|
||||
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||
_activeEntryReturnSentinelRip,
|
||||
_activeGuestReturnSlotAddress,
|
||||
(nint)hostRspSlot,
|
||||
(nint)entryStub,
|
||||
state?.AffinityMask ?? 0,
|
||||
out var yieldRequested,
|
||||
out var yieldReason,
|
||||
out var forcedExit);
|
||||
_activeGuestThreadYieldRequested = yieldRequested;
|
||||
_activeGuestThreadYieldReason = yieldReason;
|
||||
_activeForcedGuestExit = forcedExit;
|
||||
return nativeReturn;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ReturnNativeGuestExecutor(worker);
|
||||
}
|
||||
}
|
||||
|
||||
private NativeGuestExecutor? RentNativeGuestExecutor()
|
||||
{
|
||||
if (NativeGuestWorkersDisabled)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
if (_idleNativeWorkers.Count > 0)
|
||||
{
|
||||
return _idleNativeWorkers.Pop();
|
||||
}
|
||||
}
|
||||
var worker = NativeGuestExecutor.TryCreate(this);
|
||||
if (worker is null)
|
||||
{
|
||||
if (Interlocked.Exchange(ref _nativeWorkerCreationFailedLogged, 1) == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Failed to create a native guest worker thread; falling back to inline guest execution.");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
worker.Dispose();
|
||||
return null;
|
||||
}
|
||||
_allNativeWorkers.Add(worker);
|
||||
}
|
||||
return worker;
|
||||
}
|
||||
|
||||
private void ReturnNativeGuestExecutor(NativeGuestExecutor worker)
|
||||
{
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (!_nativeWorkersDisposed)
|
||||
{
|
||||
_idleNativeWorkers.Push(worker);
|
||||
return;
|
||||
}
|
||||
}
|
||||
worker.Dispose();
|
||||
}
|
||||
|
||||
private void DisposeNativeGuestExecutors()
|
||||
{
|
||||
NativeGuestExecutor[] workers;
|
||||
lock (_nativeWorkerGate)
|
||||
{
|
||||
if (_nativeWorkersDisposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_nativeWorkersDisposed = true;
|
||||
workers = _allNativeWorkers.ToArray();
|
||||
_allNativeWorkers.Clear();
|
||||
_idleNativeWorkers.Clear();
|
||||
}
|
||||
foreach (var worker in workers)
|
||||
{
|
||||
worker.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
// A pooled raw OS thread that executes guest entry stubs. The run loop is emitted
|
||||
// native code (no CLR unwind info required — nothing ever unwinds through it):
|
||||
//
|
||||
// loop: WaitForSingleObject(work);
|
||||
// if (*stopFlag) { SetEvent(done); ExitThread(0); }
|
||||
// rax = RunPrologue(self); // managed: binds per-run ambient, returns stub
|
||||
// if (rax != 0) eax = rax(); // guest entry stub — zero managed frames below
|
||||
// RunEpilogue(self, eax); // managed: captures outcome, restores ambient
|
||||
// SetEvent(done); goto loop;
|
||||
//
|
||||
// Workers carry no per-guest identity of their own: the prologue rebinds guest TLS,
|
||||
// the host-RSP slot, the Active* thread-statics and the GuestThreadExecution ambient
|
||||
// on every run, so a worker can be reused for any guest thread.
|
||||
private sealed unsafe class NativeGuestExecutor : IDisposable
|
||||
{
|
||||
private const uint LoopStubSize = 512u;
|
||||
private const uint WorkerStackReservation = 4u * 1024u * 1024u;
|
||||
|
||||
private static nint _waitForSingleObjectAddress;
|
||||
private static nint _setEventAddress;
|
||||
private static nint _exitThreadAddress;
|
||||
|
||||
private readonly DirectExecutionBackend _backend;
|
||||
private readonly AutoResetEvent _workAvailable = new(false);
|
||||
private readonly AutoResetEvent _workCompleted = new(false);
|
||||
private GCHandle _selfHandle;
|
||||
private void* _controlBlock;
|
||||
private void* _loopStub;
|
||||
private nint _threadHandle;
|
||||
private uint _nativeThreadId;
|
||||
|
||||
// Single in-flight run; publication is ordered by the work/done event pair.
|
||||
private CpuContext? _runContext;
|
||||
private GuestThreadState? _runState;
|
||||
private ulong _runGuestThreadHandle;
|
||||
private ulong _runSentinelRip;
|
||||
private ulong _runReturnSlotAddress;
|
||||
private nint _runHostRspSlot;
|
||||
private nint _runEntryStub;
|
||||
private ulong _runAffinityMask;
|
||||
private int _runNativeResult;
|
||||
private bool _runYieldRequested;
|
||||
private string? _runYieldReason;
|
||||
private bool _runForcedExit;
|
||||
private bool _runPrologueFailed;
|
||||
|
||||
// Prologue -> epilogue carry, only touched on the worker thread.
|
||||
private DirectExecutionBackend? _prevBackend;
|
||||
private CpuContext? _prevContext;
|
||||
private ulong _prevSentinel;
|
||||
private ulong _prevReturnSlot;
|
||||
private bool _prevForcedExit;
|
||||
private bool _prevYieldRequested;
|
||||
private string? _prevYieldReason;
|
||||
private GuestThreadState? _prevState;
|
||||
private ulong _prevGuestThreadHandle;
|
||||
private nint _prevHostRspSlot;
|
||||
private int _prevHostThreadId;
|
||||
private bool _entered;
|
||||
|
||||
private NativeGuestExecutor(DirectExecutionBackend backend)
|
||||
{
|
||||
_backend = backend;
|
||||
}
|
||||
|
||||
public static NativeGuestExecutor? TryCreate(DirectExecutionBackend backend)
|
||||
{
|
||||
if (!EnsureKernel32Exports())
|
||||
{
|
||||
return null;
|
||||
}
|
||||
var executor = new NativeGuestExecutor(backend);
|
||||
if (!executor.Initialize())
|
||||
{
|
||||
executor.Dispose();
|
||||
return null;
|
||||
}
|
||||
return executor;
|
||||
}
|
||||
|
||||
private static bool EnsureKernel32Exports()
|
||||
{
|
||||
if (_exitThreadAddress != 0)
|
||||
{
|
||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0;
|
||||
}
|
||||
nint kernel32 = GetModuleHandle("kernel32.dll");
|
||||
if (kernel32 == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_waitForSingleObjectAddress = GetProcAddress(kernel32, "WaitForSingleObject");
|
||||
_setEventAddress = GetProcAddress(kernel32, "SetEvent");
|
||||
_exitThreadAddress = GetProcAddress(kernel32, "ExitThread");
|
||||
return _waitForSingleObjectAddress != 0 && _setEventAddress != 0 && _exitThreadAddress != 0;
|
||||
}
|
||||
|
||||
private bool Initialize()
|
||||
{
|
||||
_selfHandle = GCHandle.Alloc(this);
|
||||
_controlBlock = VirtualAlloc(null, 4096u, 12288u, 4u);
|
||||
if (_controlBlock == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
_loopStub = VirtualAlloc(null, LoopStubSize, 12288u, 64u);
|
||||
if (_loopStub == null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
|
||||
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
|
||||
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
|
||||
var workHandle = _workAvailable.SafeWaitHandle.DangerousGetHandle();
|
||||
var doneHandle = _workCompleted.SafeWaitHandle.DangerousGetHandle();
|
||||
|
||||
byte* code = (byte*)_loopStub;
|
||||
int offset = 0;
|
||||
void Emit(byte value) => code[offset++] = value;
|
||||
void EmitMovRcxImm64(ulong value)
|
||||
{
|
||||
Emit(0x48); Emit(0xB9);
|
||||
*(ulong*)(code + offset) = value;
|
||||
offset += sizeof(ulong);
|
||||
}
|
||||
void EmitMovRaxImm64(ulong value)
|
||||
{
|
||||
Emit(0x48); Emit(0xB8);
|
||||
*(ulong*)(code + offset) = value;
|
||||
offset += sizeof(ulong);
|
||||
}
|
||||
void EmitCallRax()
|
||||
{
|
||||
Emit(0xFF); Emit(0xD0);
|
||||
}
|
||||
void EmitSetDoneEvent()
|
||||
{
|
||||
EmitMovRcxImm64((ulong)doneHandle);
|
||||
EmitMovRaxImm64((ulong)_setEventAddress);
|
||||
EmitCallRax();
|
||||
}
|
||||
|
||||
// Thread entry leaves RSP ≡ 8 (mod 16); after this every call below happens
|
||||
// at RSP ≡ 0 with shadow space available.
|
||||
Emit(0x48); Emit(0x83); Emit(0xEC); Emit(0x28); // sub rsp, 0x28
|
||||
int loopStart = offset;
|
||||
EmitMovRcxImm64((ulong)workHandle);
|
||||
Emit(0xBA); // mov edx, INFINITE
|
||||
*(uint*)(code + offset) = 0xFFFFFFFFu;
|
||||
offset += sizeof(uint);
|
||||
EmitMovRaxImm64((ulong)_waitForSingleObjectAddress);
|
||||
EmitCallRax();
|
||||
EmitMovRaxImm64((ulong)_controlBlock);
|
||||
Emit(0x83); Emit(0x38); Emit(0x00); // cmp dword [rax], 0
|
||||
Emit(0x0F); Emit(0x85); // jne stop
|
||||
int stopJump = offset;
|
||||
offset += sizeof(int);
|
||||
EmitMovRcxImm64((ulong)executorHandle);
|
||||
EmitMovRaxImm64((ulong)prologuePtr);
|
||||
EmitCallRax(); // rax = entry stub, or 0 on failure
|
||||
Emit(0x48); Emit(0x85); Emit(0xC0); // test rax, rax
|
||||
Emit(0x0F); Emit(0x84); // je skipEntry
|
||||
int skipJump = offset;
|
||||
offset += sizeof(int);
|
||||
EmitCallRax(); // guest entry stub -> eax
|
||||
int skipEntryOffset = offset;
|
||||
Emit(0x89); Emit(0xC2); // mov edx, eax
|
||||
EmitMovRcxImm64((ulong)executorHandle);
|
||||
EmitMovRaxImm64((ulong)epiloguePtr);
|
||||
EmitCallRax();
|
||||
EmitSetDoneEvent();
|
||||
Emit(0xE9); // jmp loop
|
||||
*(int*)(code + offset) = loopStart - (offset + sizeof(int));
|
||||
offset += sizeof(int);
|
||||
int stopOffset = offset;
|
||||
// Signal done so a Run() racing teardown cannot park forever; a stopped
|
||||
// worker is never re-rented, so the stale signal is unobservable.
|
||||
EmitSetDoneEvent();
|
||||
Emit(0x31); Emit(0xC9); // xor ecx, ecx
|
||||
EmitMovRaxImm64((ulong)_exitThreadAddress);
|
||||
EmitCallRax();
|
||||
Emit(0xCC); // int3 (ExitThread never returns)
|
||||
*(int*)(code + stopJump) = stopOffset - (stopJump + sizeof(int));
|
||||
*(int*)(code + skipJump) = skipEntryOffset - (skipJump + sizeof(int));
|
||||
|
||||
uint oldProtect = 0;
|
||||
if (!VirtualProtect(_loopStub, LoopStubSize, 32u, &oldProtect))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), _loopStub, LoopStubSize);
|
||||
_threadHandle = CreateThread(
|
||||
0,
|
||||
WorkerStackReservation,
|
||||
(nint)_loopStub,
|
||||
0,
|
||||
StackSizeParamIsAReservation,
|
||||
out _nativeThreadId);
|
||||
if (_threadHandle == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode($"worker_created native_tid={_nativeThreadId} loop=0x{(ulong)_loopStub:X16}");
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
public int Run(
|
||||
CpuContext context,
|
||||
GuestThreadState? state,
|
||||
ulong guestThreadHandle,
|
||||
ulong sentinelRip,
|
||||
ulong returnSlotAddress,
|
||||
nint hostRspSlot,
|
||||
nint entryStub,
|
||||
ulong affinityMask,
|
||||
out bool yieldRequested,
|
||||
out string? yieldReason,
|
||||
out bool forcedExit)
|
||||
{
|
||||
_runContext = context;
|
||||
_runState = state;
|
||||
_runGuestThreadHandle = guestThreadHandle;
|
||||
_runSentinelRip = sentinelRip;
|
||||
_runReturnSlotAddress = returnSlotAddress;
|
||||
_runHostRspSlot = hostRspSlot;
|
||||
_runEntryStub = entryStub;
|
||||
_runAffinityMask = affinityMask;
|
||||
_runPrologueFailed = true;
|
||||
_runYieldRequested = false;
|
||||
_runYieldReason = null;
|
||||
_runForcedExit = false;
|
||||
_workAvailable.Set();
|
||||
_workCompleted.WaitOne();
|
||||
_runContext = null;
|
||||
_runState = null;
|
||||
yieldRequested = _runYieldRequested;
|
||||
yieldReason = _runYieldReason;
|
||||
forcedExit = _runForcedExit;
|
||||
if (_runPrologueFailed)
|
||||
{
|
||||
throw new InvalidOperationException("Native guest worker failed to bind the run ambient (prologue fault)");
|
||||
}
|
||||
return _runNativeResult;
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static nint RunPrologue(nint executorHandle)
|
||||
{
|
||||
try
|
||||
{
|
||||
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
|
||||
return executor.EnterRun();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Native guest worker prologue failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
[UnmanagedCallersOnly]
|
||||
private static void RunEpilogue(nint executorHandle, int nativeResult)
|
||||
{
|
||||
try
|
||||
{
|
||||
var executor = (NativeGuestExecutor)GCHandle.FromIntPtr(executorHandle).Target!;
|
||||
executor.ExitRun(nativeResult);
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][ERROR] Native guest worker epilogue failed: {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
catch
|
||||
{
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private nint EnterRun()
|
||||
{
|
||||
var backend = _backend;
|
||||
_prevBackend = _activeExecutionBackend;
|
||||
_prevContext = _activeCpuContext;
|
||||
_prevSentinel = _activeEntryReturnSentinelRip;
|
||||
_prevReturnSlot = _activeGuestReturnSlotAddress;
|
||||
_prevForcedExit = _activeForcedGuestExit;
|
||||
_prevYieldRequested = _activeGuestThreadYieldRequested;
|
||||
_prevYieldReason = _activeGuestThreadYieldReason;
|
||||
_prevState = _activeGuestThreadState;
|
||||
_prevHostRspSlot = TlsGetValue(backend._hostRspSlotTlsIndex);
|
||||
_prevGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_runGuestThreadHandle);
|
||||
_entered = true;
|
||||
_activeExecutionBackend = backend;
|
||||
_activeCpuContext = _runContext;
|
||||
_activeEntryReturnSentinelRip = _runSentinelRip;
|
||||
_activeGuestReturnSlotAddress = _runReturnSlotAddress;
|
||||
_activeForcedGuestExit = false;
|
||||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
_activeGuestThreadState = _runState;
|
||||
backend.BindTlsBase(_runContext!);
|
||||
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
|
||||
if (_runState is { } state)
|
||||
{
|
||||
_prevHostThreadId = Volatile.Read(ref state.HostThreadId);
|
||||
Volatile.Write(ref state.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
||||
}
|
||||
if (_runAffinityMask != 0)
|
||||
{
|
||||
backend.ApplyGuestThreadAffinity(_runAffinityMask);
|
||||
}
|
||||
_runPrologueFailed = false;
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"worker_enter guest=0x{_runGuestThreadHandle:X16} stub=0x{(ulong)_runEntryStub:X16}");
|
||||
}
|
||||
return _runEntryStub;
|
||||
}
|
||||
|
||||
private void ExitRun(int nativeResult)
|
||||
{
|
||||
_runNativeResult = nativeResult;
|
||||
_runYieldRequested = _activeGuestThreadYieldRequested;
|
||||
_runYieldReason = _activeGuestThreadYieldReason;
|
||||
_runForcedExit = _activeForcedGuestExit;
|
||||
if (!_entered)
|
||||
{
|
||||
return;
|
||||
}
|
||||
_entered = false;
|
||||
if (_runState is { } state)
|
||||
{
|
||||
Volatile.Write(ref state.HostThreadId, _prevHostThreadId);
|
||||
}
|
||||
TlsSetValue(_backend._hostRspSlotTlsIndex, _prevHostRspSlot);
|
||||
GuestThreadExecution.RestoreGuestThread(_prevGuestThreadHandle);
|
||||
_activeExecutionBackend = _prevBackend;
|
||||
_activeCpuContext = _prevContext;
|
||||
_activeEntryReturnSentinelRip = _prevSentinel;
|
||||
_activeGuestReturnSlotAddress = _prevReturnSlot;
|
||||
_activeForcedGuestExit = _prevForcedExit;
|
||||
_activeGuestThreadYieldRequested = _prevYieldRequested;
|
||||
_activeGuestThreadYieldReason = _prevYieldReason;
|
||||
_activeGuestThreadState = _prevState;
|
||||
_prevBackend = null;
|
||||
_prevContext = null;
|
||||
_prevState = null;
|
||||
_prevYieldReason = null;
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X8} yield={_runYieldRequested}");
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (_controlBlock != null)
|
||||
{
|
||||
*(int*)_controlBlock = 1;
|
||||
}
|
||||
try
|
||||
{
|
||||
_workAvailable.Set();
|
||||
}
|
||||
catch (ObjectDisposedException)
|
||||
{
|
||||
}
|
||||
var exited = _threadHandle == 0;
|
||||
if (_threadHandle != 0)
|
||||
{
|
||||
exited = WaitForSingleObject(_threadHandle, 1000u) == 0u;
|
||||
CloseHandle(_threadHandle);
|
||||
_threadHandle = 0;
|
||||
}
|
||||
if (!exited)
|
||||
{
|
||||
// The worker is still parked in guest code (teardown should have unwound
|
||||
// it first). Leak the stub, control block, events and GC handle rather
|
||||
// than have the thread execute freed memory.
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Native guest worker tid={_nativeThreadId} did not stop; leaking its run loop.");
|
||||
return;
|
||||
}
|
||||
if (_loopStub != null)
|
||||
{
|
||||
VirtualFree(_loopStub, 0u, 32768u);
|
||||
_loopStub = null;
|
||||
}
|
||||
if (_controlBlock != null)
|
||||
{
|
||||
VirtualFree(_controlBlock, 0u, 32768u);
|
||||
_controlBlock = null;
|
||||
}
|
||||
if (_selfHandle.IsAllocated)
|
||||
{
|
||||
_selfHandle.Free();
|
||||
}
|
||||
_workAvailable.Dispose();
|
||||
_workCompleted.Dispose();
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -11,13 +11,11 @@ using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, IGuestThreadScheduler, IDisposable
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Native");
|
||||
private const int ImportLoopHistoryLength = 2048;
|
||||
|
||||
private const int ImportLoopWideDiversityWindow = 768;
|
||||
@@ -424,10 +422,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
private void ThreadMain()
|
||||
{
|
||||
var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_guestThreadHandle);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode($"runner_start guest=0x{_guestThreadHandle:X16}");
|
||||
}
|
||||
try
|
||||
{
|
||||
while (true)
|
||||
@@ -438,11 +432,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return;
|
||||
}
|
||||
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeCycleId = Interlocked.Increment(ref _threadModeCycleCounter);
|
||||
TraceThreadMode($"runner_run guest=0x{_guestThreadHandle:X16}");
|
||||
}
|
||||
try
|
||||
{
|
||||
_work?.Invoke();
|
||||
@@ -455,10 +444,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode($"runner_stop guest=0x{_guestThreadHandle:X16}");
|
||||
}
|
||||
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
|
||||
}
|
||||
}
|
||||
@@ -482,14 +467,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private readonly Queue<GuestThreadState> _readyGuestThreads = new Queue<GuestThreadState>();
|
||||
|
||||
// Once set, guest worker threads are unwound to the host at their next import
|
||||
// dispatch and Pump refuses to start new ones. This must happen before the
|
||||
// runtime frees trampolines or guest memory: workers that keep running native
|
||||
// guest code during teardown execute freed pages (observed as an execute-AV in
|
||||
// a MEM_FREE module region plus a CLR "UnmanagedCallersOnly" fatal from a Pump
|
||||
// thread entering a freed stub).
|
||||
private volatile bool _guestTeardownRequested;
|
||||
|
||||
private int _readyGuestThreadCount;
|
||||
|
||||
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
|
||||
@@ -588,33 +565,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private static int _nestedVehTraceCount;
|
||||
|
||||
// SHARPEMU_LOG_THREAD_MODE=1 — GC thread-mode corruption investigation. Traces
|
||||
// every managed<->guest transition per guest-thread run cycle so the last event
|
||||
// before a ReversePInvokeBadTransition FailFast identifies the corrupting path.
|
||||
private static readonly bool LogThreadMode =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_THREAD_MODE"), "1", StringComparison.Ordinal);
|
||||
|
||||
private static long _threadModeCycleCounter;
|
||||
|
||||
[ThreadStatic]
|
||||
private static long _threadModeCycleId;
|
||||
|
||||
[ThreadStatic]
|
||||
private static int _threadModeGatewayDepth;
|
||||
|
||||
[ThreadStatic]
|
||||
private static long _threadModeGatewayCalls;
|
||||
|
||||
[ThreadStatic]
|
||||
private static bool _threadModeGatewayFirstLogged;
|
||||
|
||||
private static void TraceThreadMode(string message)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[THREADMODE] {message} cycle={_threadModeCycleId} tid={GetCurrentThreadId()} managed={Environment.CurrentManagedThreadId}");
|
||||
Console.Error.Flush();
|
||||
}
|
||||
|
||||
private const uint MEM_COMMIT = 4096u;
|
||||
|
||||
private const uint MEM_RESERVE = 8192u;
|
||||
@@ -673,17 +623,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
private unsafe static int CallNativeEntry(void* entry)
|
||||
{
|
||||
var nativeEntry = (delegate* unmanaged[Cdecl]<int>)entry;
|
||||
if (!LogThreadMode)
|
||||
{
|
||||
return nativeEntry();
|
||||
}
|
||||
|
||||
_threadModeGatewayFirstLogged = false;
|
||||
var dispatchesBefore = _threadModeGatewayCalls;
|
||||
TraceThreadMode($"native_enter entry=0x{(ulong)entry:X16}");
|
||||
var result = nativeEntry();
|
||||
TraceThreadMode($"native_exit result=0x{result:X8} dispatches={_threadModeGatewayCalls - dispatchesBefore}");
|
||||
return result;
|
||||
return nativeEntry();
|
||||
}
|
||||
|
||||
private unsafe static void WriteCtxU64(void* contextRecord, int offset, ulong value)
|
||||
@@ -882,7 +822,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
_importLoopGuardSeconds = GetImportLoopGuardSeconds();
|
||||
_entryReturnSentinelRip = 0uL;
|
||||
_forcedGuestExit = false;
|
||||
_guestTeardownRequested = false;
|
||||
_importLoopSignatureCount = 0;
|
||||
_importLoopSignatureWriteIndex = 0;
|
||||
_importLoopPatternHits = 0;
|
||||
@@ -1181,72 +1120,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
0x75, 0xF5,
|
||||
0xC3,
|
||||
],
|
||||
// memcpy: guarded native copy. The first "rep movsb" intrinsic had no bounds/null
|
||||
// checking and crashed with a read at -1 right after a null-dst memset recovery in
|
||||
// the NGS2 audio streaming code path, so it was temporarily pulled in favor of the
|
||||
// safe C# HLE memcpy. That detour costs a full import dispatch per call - far too
|
||||
// slow for a function this hot - so this stub keeps the native leaf path and adds
|
||||
// the same guards as memset below: it silently returns dst without copying when dst
|
||||
// or src is null/low-page (< 0x10000) or outside canonical user space, or when len
|
||||
// is 0 or absurd (> 512MB).
|
||||
"Q3VBxCXhUHs" =>
|
||||
[
|
||||
0x48, 0x89, 0xF8, // mov rax, rdi (return dst)
|
||||
0x48, 0x81, 0xFF, 0x00, 0x00, 0x01, 0x00, // cmp rdi, 0x10000
|
||||
0x72, 0x31, // jb done
|
||||
0x49, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, // mov r8, 0x800000000000
|
||||
0x4C, 0x39, 0xC7, // cmp rdi, r8
|
||||
0x73, 0x22, // jae done
|
||||
0x48, 0x81, 0xFE, 0x00, 0x00, 0x01, 0x00, // cmp rsi, 0x10000
|
||||
0x72, 0x19, // jb done
|
||||
0x4C, 0x39, 0xC6, // cmp rsi, r8
|
||||
0x73, 0x14, // jae done
|
||||
0x48, 0x81, 0xFA, 0x00, 0x00, 0x00, 0x20, // cmp rdx, 0x20000000
|
||||
0x77, 0x0B, // ja done
|
||||
0x48, 0x85, 0xD2, // test rdx, rdx
|
||||
0x74, 0x06, // jz done
|
||||
0x48, 0x89, 0xD1, // mov rcx, rdx
|
||||
0xFC, // cld
|
||||
0xF3, 0xA4, // rep movsb
|
||||
0xC3, // done: ret
|
||||
],
|
||||
// memset: guarded native fill. An earlier unguarded version crashed with a write AV
|
||||
// at address 0 (NGS2 audio streaming init memsets a never-populated buffer field),
|
||||
// so this one mirrors the HLE guards and silently returns dst without writing when
|
||||
// dst is null/low-page (< 0x10000), dst is outside canonical user space, or len is
|
||||
// absurd (> 512MB, e.g. the 0x27060035 / sign-extended values NGS2 passes). Routing
|
||||
// memset through the HLE trampoline instead is not viable: parse/streaming loops
|
||||
// issue hundreds of thousands of small memsets back-to-back, which crawls at
|
||||
// dispatch speed and looks like a repeating-import hang to the loop guard.
|
||||
// _sigprocmask: the HLE handler (KernelRuntimeCompatExports.Sigprocmask) is a pure
|
||||
// no-op returning 0 that never writes oldset, so this is behavior-identical. The
|
||||
// game's bundled libc queries the mask (set=NULL) once per iteration in its font/
|
||||
// parse loops - hundreds of thousands of back-to-back calls that both crawl at
|
||||
// dispatch speed and read as a repeating-import hang to the loop guard.
|
||||
"6xVpy0Fdq+I" =>
|
||||
[
|
||||
0x31, 0xC0, // xor eax, eax
|
||||
0xC3, // ret
|
||||
0x48, 0x89, 0xF8,
|
||||
0x48, 0x89, 0xD1,
|
||||
0xF3, 0xA4,
|
||||
0xC3,
|
||||
],
|
||||
"8zTFvBIAIN8" =>
|
||||
[
|
||||
0x48, 0x89, 0xF8, // mov rax, rdi (return dst)
|
||||
0x48, 0x81, 0xFF, 0x00, 0x00, 0x01, 0x00, // cmp rdi, 0x10000
|
||||
0x72, 0x2B, // jb done
|
||||
0x49, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, // mov r8, 0x800000000000
|
||||
0x4C, 0x39, 0xC7, // cmp rdi, r8
|
||||
0x73, 0x1C, // jae done
|
||||
0x48, 0x81, 0xFA, 0x00, 0x00, 0x00, 0x20, // cmp rdx, 0x20000000
|
||||
0x77, 0x13, // ja done
|
||||
0x48, 0x85, 0xD2, // test rdx, rdx
|
||||
0x74, 0x0E, // jz done
|
||||
0x48, 0x89, 0xD1, // mov rcx, rdx
|
||||
0x49, 0x89, 0xF9, // mov r9, rdi
|
||||
0x89, 0xF0, // mov eax, esi
|
||||
0xFC, // cld
|
||||
0xF3, 0xAA, // rep stosb
|
||||
0x4C, 0x89, 0xC8, // mov rax, r9
|
||||
0xC3, // done: ret
|
||||
0x49, 0x89, 0xF8,
|
||||
0x48, 0x89, 0xF0,
|
||||
0x48, 0x89, 0xD1,
|
||||
0xF3, 0xAA,
|
||||
0x4C, 0x89, 0xC0,
|
||||
0xC3,
|
||||
],
|
||||
_ => default,
|
||||
};
|
||||
@@ -1465,25 +1353,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
{
|
||||
return exportName switch
|
||||
{
|
||||
"memcpy" or
|
||||
"memmove" or
|
||||
// memset/memcpy excluded: the raw LLE routines have no null/bounds guard and crash
|
||||
// with an access violation on bad pointers (observed hit during Quake's CL_Init,
|
||||
// where a still-unidentified upstream bug calls memcpy/memset with a null
|
||||
// destination). Both are instead served by the guarded native intrinsics in
|
||||
// TryCreateNativeImportIntrinsic, which fail safely without leaving the leaf path.
|
||||
"memcmp" or
|
||||
// _Getpctype must come from the game's own Dinkumware libc when one is bundled:
|
||||
// it returns a pointer to that CRT's ctype bitmask table, whose bit layout
|
||||
// (_DI=0x20, _SP=0x04, _BB=0x80, ...) differs from the MSVC-style table the HLE
|
||||
// fallback used to serve. Serving the wrong layout made the bundled printf engine
|
||||
// render every Sys_Error message as an empty string (isdigit misfired during
|
||||
// %-directive parsing) and made mcpp drop 'a'-'f' from identifiers ("texture" ->
|
||||
// "txtur", the 0x80 bit reads as _BB/control there). _Getptolower/_Getptoupper
|
||||
// already resolve to the bundled module because no HLE export shadows them; this
|
||||
// keeps _Getpctype consistent with them. It is a pure accessor returning a pointer
|
||||
// to a const table, so it is also the cheapest possible LLE call - important
|
||||
// because parsers hit it once per input character.
|
||||
"_Getpctype" => true,
|
||||
"memset" or
|
||||
"memcmp" => true,
|
||||
_ => false,
|
||||
};
|
||||
}
|
||||
@@ -1695,7 +1568,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
private unsafe nint CreateImportHandlerTrampoline(int importIndex)
|
||||
{
|
||||
void* ptr = VirtualAlloc(null, 256u, 12288u, 64u);
|
||||
void* ptr = VirtualAlloc(null, 192u, 12288u, 64u);
|
||||
if (ptr == null)
|
||||
{
|
||||
return 0;
|
||||
@@ -1723,21 +1596,20 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ptr2[num++] = 82;
|
||||
ptr2[num++] = 86;
|
||||
ptr2[num++] = 87;
|
||||
// sub rsp, 0x80 — reserve 8*16 bytes for the SysV variadic XMM save area
|
||||
ptr2[num++] = 0x48; ptr2[num++] = 0x81; ptr2[num++] = 0xEC;
|
||||
ptr2[num++] = 0x80; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 0x00;
|
||||
// movdqu [rsp + i*0x10], xmm{i} for i = 0..7 (F3 0F 7F /r, SIB=0x24 base=rsp, disp8)
|
||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x44; ptr2[num++] = 0x24; ptr2[num++] = 0x00; // xmm0
|
||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x4C; ptr2[num++] = 0x24; ptr2[num++] = 0x10; // xmm1
|
||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x54; ptr2[num++] = 0x24; ptr2[num++] = 0x20; // xmm2
|
||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x5C; ptr2[num++] = 0x24; ptr2[num++] = 0x30; // xmm3
|
||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x64; ptr2[num++] = 0x24; ptr2[num++] = 0x40; // xmm4
|
||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x6C; ptr2[num++] = 0x24; ptr2[num++] = 0x50; // xmm5
|
||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x74; ptr2[num++] = 0x24; ptr2[num++] = 0x60; // xmm6
|
||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x7C; ptr2[num++] = 0x24; ptr2[num++] = 0x70; // xmm7
|
||||
// lea r12, [rsp + 0x80] — r12 = argpack base (the 12 pushed GP regs), past the XMM area
|
||||
ptr2[num++] = 0x4C; ptr2[num++] = 0x8D; ptr2[num++] = 0xA4; ptr2[num++] = 0x24;
|
||||
ptr2[num++] = 0x80; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 0x00;
|
||||
ptr2[num++] = 72;
|
||||
ptr2[num++] = 131;
|
||||
ptr2[num++] = 236;
|
||||
ptr2[num++] = 16;
|
||||
ptr2[num++] = 243;
|
||||
ptr2[num++] = 15;
|
||||
ptr2[num++] = 127;
|
||||
ptr2[num++] = 4;
|
||||
ptr2[num++] = 36;
|
||||
ptr2[num++] = 76;
|
||||
ptr2[num++] = 141;
|
||||
ptr2[num++] = 100;
|
||||
ptr2[num++] = 36;
|
||||
ptr2[num++] = 16;
|
||||
ptr2[num++] = 72;
|
||||
ptr2[num++] = 131;
|
||||
ptr2[num++] = 236;
|
||||
@@ -1785,11 +1657,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ptr2[num++] = 131;
|
||||
ptr2[num++] = 196;
|
||||
ptr2[num++] = 40;
|
||||
// movdqu xmm0, [r12 - 0x80] — reload the return XMM0 the gateway wrote into the
|
||||
// argpack's xmm0 save slot (float/double returns: powf/logf/wcstod). SysV/Win64
|
||||
// XMM regs are volatile across calls, so an unconditional reload is ABI-safe.
|
||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x41; ptr2[num++] = 0x0F; ptr2[num++] = 0x6F;
|
||||
ptr2[num++] = 0x84; ptr2[num++] = 0x24; ptr2[num++] = 0x80; ptr2[num++] = 0xFF; ptr2[num++] = 0xFF; ptr2[num++] = 0xFF;
|
||||
ptr2[num++] = 76;
|
||||
ptr2[num++] = 137;
|
||||
ptr2[num++] = 228;
|
||||
@@ -1812,14 +1679,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ptr2[num++] = 65;
|
||||
ptr2[num++] = 95;
|
||||
ptr2[num++] = 195;
|
||||
uint num2 = default(uint);
|
||||
if (!VirtualProtect(ptr, 256u, 32u, &num2))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, 256u);
|
||||
return (nint)ptr;
|
||||
uint num2 = default(uint);
|
||||
VirtualProtect(ptr, 192u, 32u, &num2);
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
|
||||
return (nint)ptr;
|
||||
}
|
||||
catch
|
||||
{
|
||||
@@ -1900,11 +1763,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
tlsHandlerAddress[num++] = 195;
|
||||
_tlsPatchStubOffset = (num + 15) & ~15;
|
||||
uint num2 = default(uint);
|
||||
if (!VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS handler at 0x{_tlsHandlerAddress:X16}");
|
||||
return;
|
||||
}
|
||||
VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2);
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, TlsHandlerRegionSize);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] TLS handler at 0x{_tlsHandlerAddress:X16}");
|
||||
}
|
||||
@@ -1925,11 +1784,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ptr2[i] = 144;
|
||||
}
|
||||
uint num = default(uint);
|
||||
if (!VirtualProtect(ptr, 4096u, 32u, &num))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for unresolved return stub at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
VirtualProtect(ptr, 4096u, 32u, &num);
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, 16u);
|
||||
return (nint)ptr;
|
||||
}
|
||||
@@ -1976,11 +1831,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
EmitByte(code, ref offset, 0xC3);
|
||||
|
||||
uint oldProtect = default;
|
||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for guest return stub at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||
return (nint)ptr;
|
||||
}
|
||||
@@ -1996,36 +1847,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
byte* code = (byte*)ptr;
|
||||
int offset = 0;
|
||||
// Native pre-filter: these exception codes are raised while the thread can be in
|
||||
// cooperative GC mode (a C# throw is RaiseException(0xE0434352) on the throwing
|
||||
// thread; FailFast/stack-overflow arrive mid-runtime-failure). Entering the managed
|
||||
// handler then trips the CLR's reverse-P/Invoke check and kills the process with
|
||||
// "Invalid Program: attempted to call a UnmanagedCallersOnly method from managed
|
||||
// code" — this is why no managed throw (even one with a catch handler) ever
|
||||
// survived inside the emulator. Continue the handler search without touching
|
||||
// managed code; the CLR's own VEH handles its exceptions. MSVC C++ exceptions
|
||||
// (Vulkan drivers, host CRT) are excluded too: the managed handler only ever
|
||||
// returned CONTINUE_SEARCH for them.
|
||||
ReadOnlySpan<uint> nonManagedExceptionCodes = [0xE0434352u, MSVC_CPP_EXCEPTION, 0xC0000409u, 0xC00000FDu];
|
||||
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
|
||||
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
|
||||
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
EmitByte(code, ref offset, 0x3D); // cmp eax, imm32
|
||||
EmitUInt32(code, ref offset, nonManagedExceptionCodes[i]);
|
||||
EmitByte(code, ref offset, 0x74); // je pass
|
||||
passJumpOffsets[i] = offset;
|
||||
EmitByte(code, ref offset, 0x00);
|
||||
}
|
||||
EmitByte(code, ref offset, 0xEB); EmitByte(code, ref offset, 0x03); // jmp over pass block
|
||||
int passOffset = offset;
|
||||
EmitByte(code, ref offset, 0x31); EmitByte(code, ref offset, 0xC0); // pass: xor eax, eax (EXCEPTION_CONTINUE_SEARCH)
|
||||
EmitByte(code, ref offset, 0xC3); // ret
|
||||
for (int i = 0; i < nonManagedExceptionCodes.Length; i++)
|
||||
{
|
||||
code[passJumpOffsets[i]] = checked((byte)(passOffset - (passJumpOffsets[i] + 1)));
|
||||
}
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x54); // push r12
|
||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
|
||||
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xE4); // mov r12, rsp
|
||||
@@ -2102,11 +1923,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
|
||||
|
||||
uint oldProtect = default;
|
||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
|
||||
return 0;
|
||||
}
|
||||
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||
return (nint)ptr;
|
||||
}
|
||||
@@ -2437,11 +2254,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ptr[num2++] = 144;
|
||||
}
|
||||
uint flNewProtect = default(uint);
|
||||
if (!VirtualProtect((void*)num, 32u, 32u, &flNewProtect))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS store helper at 0x{num:X16}");
|
||||
return 0;
|
||||
}
|
||||
VirtualProtect((void*)num, 32u, 32u, &flNewProtect);
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)num, 32u);
|
||||
return num;
|
||||
}
|
||||
@@ -2710,10 +2523,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
public void Pump(CpuContext callerContext, string reason)
|
||||
{
|
||||
_ = callerContext;
|
||||
if (_guestTeardownRequested)
|
||||
{
|
||||
return;
|
||||
}
|
||||
var runSynchronously = string.Equals(reason, "entry_return", StringComparison.Ordinal);
|
||||
WakeExpiredBlockedGuestThreads();
|
||||
if (Volatile.Read(ref _readyGuestThreadCount) == 0)
|
||||
@@ -3238,45 +3047,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
continuationThread.Join();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Parks every guest worker thread before the caller starts freeing executable
|
||||
/// memory. Workers unwind to the host at their next import dispatch (see the
|
||||
/// teardown check in DispatchImport); this waits for their host threads to
|
||||
/// finish within <paramref name="timeoutMs"/>. Returns false when at least one
|
||||
/// worker is still running — the caller must then leak its executable stubs
|
||||
/// rather than free memory a live thread may still execute.
|
||||
/// </summary>
|
||||
private bool RequestGuestThreadTeardown(int timeoutMs)
|
||||
{
|
||||
_guestTeardownRequested = true;
|
||||
Thread[] hostThreads;
|
||||
lock (_guestThreadGate)
|
||||
{
|
||||
_readyGuestThreads.Clear();
|
||||
Interlocked.Exchange(ref _readyGuestThreadCount, 0);
|
||||
hostThreads = _guestThreads.Values
|
||||
.Select(static thread => thread.HostThread)
|
||||
.Where(static host => host is not null && host != Thread.CurrentThread && host.IsAlive)
|
||||
.Cast<Thread>()
|
||||
.ToArray();
|
||||
}
|
||||
|
||||
var deadline = Environment.TickCount64 + timeoutMs;
|
||||
var allStopped = true;
|
||||
foreach (var host in hostThreads)
|
||||
{
|
||||
var remaining = (int)Math.Max(1L, deadline - Environment.TickCount64);
|
||||
if (!host.Join(remaining) && host.IsAlive)
|
||||
{
|
||||
allStopped = false;
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] Guest worker host thread '{host.Name}' still running after teardown wait.");
|
||||
}
|
||||
}
|
||||
|
||||
return allStopped;
|
||||
}
|
||||
|
||||
private void ClearGuestThreads()
|
||||
{
|
||||
GuestContinuationRunner[] runners;
|
||||
@@ -3556,13 +3326,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
ApplyGuestThreadAffinity(thread.AffinityMask);
|
||||
Volatile.Write(ref thread.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
||||
_activeGuestThreadState = thread;
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeCycleId = Interlocked.Increment(ref _threadModeCycleCounter);
|
||||
TraceThreadMode(
|
||||
$"cycle_start name='{thread.Name}' guest=0x{thread.ThreadHandle:X16} reason={reason} " +
|
||||
$"rsp_slot=0x{(ulong)TlsGetValue(_hostRspSlotTlsIndex):X}");
|
||||
}
|
||||
try
|
||||
{
|
||||
LastError = null;
|
||||
@@ -3637,13 +3400,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"cycle_end name='{thread.Name}' state={thread.State} " +
|
||||
$"imports={Interlocked.Read(ref thread.ImportCount)} " +
|
||||
$"rsp_slot=0x{(ulong)TlsGetValue(_hostRspSlotTlsIndex):X}");
|
||||
}
|
||||
_activeGuestThreadState = previousGuestThreadState;
|
||||
Volatile.Write(ref thread.HostThreadId, 0);
|
||||
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
|
||||
@@ -3718,175 +3474,169 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
var previousYieldRequested = _activeGuestThreadYieldRequested;
|
||||
var previousYieldReason = _activeGuestThreadYieldReason;
|
||||
nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"entry_setup name='{name}' entry=0x{entryPoint:X16} stub=0x{(ulong)ptr:X16} " +
|
||||
$"guest_rsp=0x{context[CpuRegister.Rsp]:X16} rsp_slot_prev=0x{(ulong)previousHostRspSlotValue:X}");
|
||||
}
|
||||
try
|
||||
{
|
||||
_activeExecutionBackend = this;
|
||||
_activeCpuContext = context;
|
||||
_activeEntryReturnSentinelRip = 0;
|
||||
_activeGuestReturnSlotAddress = 0;
|
||||
_activeForcedGuestExit = false;
|
||||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
BindTlsBase(context);
|
||||
byte* ptr2 = (byte*)ptr;
|
||||
ulong hostRspSlot = (ulong)ptr + stubSize - 16uL;
|
||||
int offset = 0;
|
||||
ptr2[offset++] = 83;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 87;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 84;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 87;
|
||||
EmitHostNonvolatileXmmSave(ptr2, ref offset);
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 34;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 236;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 189;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 199;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 198;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 194;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 193;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = entryPoint;
|
||||
offset += 8;
|
||||
ptr2[offset++] = byte.MaxValue;
|
||||
ptr2[offset++] = 208;
|
||||
int sentinelOffset = offset + 4;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 139;
|
||||
ptr2[offset++] = 34;
|
||||
EmitHostNonvolatileXmmRestore(ptr2, ref offset);
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 92;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 91;
|
||||
ptr2[offset++] = 195;
|
||||
ulong sentinel = (ulong)ptr + (ulong)sentinelOffset;
|
||||
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
|
||||
_activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL;
|
||||
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel))
|
||||
{
|
||||
reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||
{
|
||||
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot);
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
return GuestNativeCallExitReason.Blocked;
|
||||
}
|
||||
if (ActiveForcedGuestExit)
|
||||
{
|
||||
reason = LastError ?? "guest thread forced exit";
|
||||
return GuestNativeCallExitReason.ForcedExit;
|
||||
}
|
||||
reason = $"returned 0x{nativeReturn:X8}";
|
||||
return GuestNativeCallExitReason.Returned;
|
||||
}
|
||||
catch (AccessViolationException ex)
|
||||
{
|
||||
reason = "access violation: " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reason = ex.GetType().Name + ": " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
finally
|
||||
{
|
||||
TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue);
|
||||
RestoreActiveExecutionThread(
|
||||
previousActiveBackend,
|
||||
previousActiveContext,
|
||||
previousSentinel,
|
||||
previousReturnSlotAddress,
|
||||
previousForcedExit,
|
||||
previousYieldRequested,
|
||||
previousYieldReason);
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
}
|
||||
}
|
||||
{
|
||||
_activeExecutionBackend = this;
|
||||
_activeCpuContext = context;
|
||||
_activeEntryReturnSentinelRip = 0;
|
||||
_activeGuestReturnSlotAddress = 0;
|
||||
_activeForcedGuestExit = false;
|
||||
_activeGuestThreadYieldRequested = false;
|
||||
_activeGuestThreadYieldReason = null;
|
||||
BindTlsBase(context);
|
||||
byte* ptr2 = (byte*)ptr;
|
||||
ulong hostRspSlot = (ulong)ptr + stubSize - 16uL;
|
||||
int offset = 0;
|
||||
ptr2[offset++] = 83;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 87;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 84;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 85;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 86;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 87;
|
||||
EmitHostNonvolatileXmmSave(ptr2, ref offset);
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 34;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 236;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 189;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 199;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 198;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 194;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx];
|
||||
offset += 8;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 137;
|
||||
ptr2[offset++] = 193;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 184;
|
||||
*(ulong*)(ptr2 + offset) = entryPoint;
|
||||
offset += 8;
|
||||
ptr2[offset++] = byte.MaxValue;
|
||||
ptr2[offset++] = 208;
|
||||
int sentinelOffset = offset + 4;
|
||||
ptr2[offset++] = 72;
|
||||
ptr2[offset++] = 131;
|
||||
ptr2[offset++] = 196;
|
||||
ptr2[offset++] = 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 186;
|
||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||
offset += 8;
|
||||
ptr2[offset++] = 73;
|
||||
ptr2[offset++] = 139;
|
||||
ptr2[offset++] = 34;
|
||||
EmitHostNonvolatileXmmRestore(ptr2, ref offset);
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 65;
|
||||
ptr2[offset++] = 92;
|
||||
ptr2[offset++] = 94;
|
||||
ptr2[offset++] = 95;
|
||||
ptr2[offset++] = 93;
|
||||
ptr2[offset++] = 91;
|
||||
ptr2[offset++] = 195;
|
||||
ulong sentinel = (ulong)ptr + (ulong)sentinelOffset;
|
||||
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
|
||||
_activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL;
|
||||
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel))
|
||||
{
|
||||
reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
try
|
||||
{
|
||||
var nativeReturn = CallNativeEntry(ptr);
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
return GuestNativeCallExitReason.Blocked;
|
||||
}
|
||||
if (ActiveForcedGuestExit)
|
||||
{
|
||||
reason = LastError ?? "guest thread forced exit";
|
||||
return GuestNativeCallExitReason.ForcedExit;
|
||||
}
|
||||
reason = $"returned 0x{nativeReturn:X8}";
|
||||
return GuestNativeCallExitReason.Returned;
|
||||
}
|
||||
catch (AccessViolationException ex)
|
||||
{
|
||||
reason = "access violation: " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
reason = ex.GetType().Name + ": " + ex.Message;
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue);
|
||||
RestoreActiveExecutionThread(
|
||||
previousActiveBackend,
|
||||
previousActiveContext,
|
||||
previousSentinel,
|
||||
previousReturnSlotAddress,
|
||||
previousForcedExit,
|
||||
previousYieldRequested,
|
||||
previousYieldReason);
|
||||
VirtualFree(ptr, 0u, 32768u);
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe GuestNativeCallExitReason ExecuteGuestContinuationEntry(
|
||||
CpuContext context,
|
||||
@@ -3916,12 +3666,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
var previousYieldRequested = _activeGuestThreadYieldRequested;
|
||||
var previousYieldReason = _activeGuestThreadYieldReason;
|
||||
nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"continuation_setup name='{name}' resume=0x{entryPoint:X16} stub=0x{(ulong)ptr:X16} " +
|
||||
$"guest_rsp=0x{context[CpuRegister.Rsp]:X16} rsp_slot_prev=0x{(ulong)previousHostRspSlotValue:X}");
|
||||
}
|
||||
try
|
||||
{
|
||||
_activeExecutionBackend = this;
|
||||
@@ -3984,18 +3728,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
uint oldProtect = default(uint);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
||||
{
|
||||
reason = $"VirtualProtect failed for guest continuation stub at 0x{(nint)ptr:X16}";
|
||||
return GuestNativeCallExitReason.Exception;
|
||||
}
|
||||
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||
ActiveGuestThreadYieldRequested = false;
|
||||
ActiveGuestThreadYieldReason = null;
|
||||
|
||||
try
|
||||
{
|
||||
var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot);
|
||||
var nativeReturn = CallNativeEntry(ptr);
|
||||
if (ActiveGuestThreadYieldRequested)
|
||||
{
|
||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||
@@ -4248,12 +3988,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
return false;
|
||||
}
|
||||
uint num5 = default(uint);
|
||||
if (!VirtualProtect(ptr, stubSize, 64u, &num5))
|
||||
{
|
||||
LastError = $"VirtualProtect failed for guest entry stub at 0x{(nint)ptr:X16}";
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
return false;
|
||||
}
|
||||
VirtualProtect(ptr, stubSize, 64u, &num5);
|
||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||
if (_hostRspSlotStorage != 0)
|
||||
{
|
||||
@@ -4271,7 +4006,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
int num6 = -1;
|
||||
try
|
||||
{
|
||||
num6 = RunGuestEntryStub(ptr, num2);
|
||||
num6 = CallNativeEntry(ptr);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Guest returned: {num6}");
|
||||
PumpUntilGuestThreadsIdle(context, "entry_return");
|
||||
}
|
||||
@@ -4298,7 +4033,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
LastError = "Detected repeating import loop and forced guest unwind to host.";
|
||||
}
|
||||
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
||||
RequestGuestThreadTeardown(3000);
|
||||
return false;
|
||||
}
|
||||
if (num6 == 0)
|
||||
@@ -4313,7 +4047,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
LastError = $"Guest entry point returned non-zero: {num6}";
|
||||
}
|
||||
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
||||
RequestGuestThreadTeardown(3000);
|
||||
return false;
|
||||
}
|
||||
finally
|
||||
@@ -4638,18 +4371,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
||||
|
||||
public unsafe void Dispose()
|
||||
{
|
||||
if (!RequestGuestThreadTeardown(2000))
|
||||
{
|
||||
// A guest worker is still executing native code; freeing the trampolines,
|
||||
// exception-handler stubs, or GC handles under it turns process exit into
|
||||
// an execute-AV / CLR fatal. Leak them — the process is going away anyway.
|
||||
Console.Error.WriteLine(
|
||||
"[LOADER][WARN] Skipping executable stub teardown: guest worker threads are still running.");
|
||||
return;
|
||||
}
|
||||
// Native guest workers park idle once every guest thread has unwound; stop
|
||||
// them before any executable stub or TLS index they reference is freed.
|
||||
DisposeNativeGuestExecutors();
|
||||
ClearImportHandlerTrampolines();
|
||||
_importEntries = Array.Empty<ImportStubEntry>();
|
||||
_runtimeSymbolsByName.Clear();
|
||||
|
||||
@@ -10,13 +10,11 @@ using SharpEmu.Core;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Loader;
|
||||
|
||||
public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
||||
private const uint SelfMagic = 0x4F153D1D;
|
||||
private const ulong SelfSegmentFlag = 0x800;
|
||||
private const int PageSize = 0x1000;
|
||||
@@ -157,7 +155,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
}
|
||||
|
||||
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
|
||||
Log.Debug($"TLS load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
|
||||
Console.Error.WriteLine($"[LOADER][TLS] load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
|
||||
|
||||
var loadContext = ParseLayout(imageData);
|
||||
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
||||
@@ -257,7 +255,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
_nextTlsModuleId++;
|
||||
}
|
||||
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||
|
||||
return new SelfImage(
|
||||
loadContext.IsSelf,
|
||||
@@ -405,15 +403,15 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
var virtualAddress = header.VirtualAddress + imageBase;
|
||||
|
||||
Log.Debug($"Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
||||
Console.Error.WriteLine($"[LOADER] Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
||||
if (header.Alignment > 1)
|
||||
{
|
||||
var vaddrMod = virtualAddress % header.Alignment;
|
||||
var offsetMod = header.Offset % header.Alignment;
|
||||
if (vaddrMod != offsetMod)
|
||||
{
|
||||
Log.Warning(
|
||||
$"Segment {index} ELF alignment mismatch! " +
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
|
||||
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
||||
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
|
||||
}
|
||||
@@ -673,13 +671,13 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
||||
{
|
||||
Log.Debug(
|
||||
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -691,8 +689,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
||||
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
||||
{
|
||||
Log.Debug(
|
||||
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -785,15 +783,15 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||
{
|
||||
Log.Debug(
|
||||
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||
}
|
||||
|
||||
if (!IsSupportedRelocationType(relocation.Type))
|
||||
{
|
||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||
{
|
||||
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocation.Type}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -802,7 +800,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||
{
|
||||
Log.Debug("[FOCUS][SKIP] target address not mapped");
|
||||
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -842,7 +840,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||
{
|
||||
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -854,9 +852,10 @@ public sealed class SelfLoader : ISelfLoader
|
||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||
if (symbolAddress == 0)
|
||||
{
|
||||
Log.Warning(
|
||||
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||
continue;
|
||||
}
|
||||
|
||||
descriptors.Add(new RelocationDescriptor(
|
||||
@@ -874,9 +873,10 @@ public sealed class SelfLoader : ISelfLoader
|
||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||
if (symbolAddress == 0)
|
||||
{
|
||||
Log.Warning(
|
||||
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||
continue;
|
||||
}
|
||||
|
||||
descriptors.Add(new RelocationDescriptor(
|
||||
@@ -893,7 +893,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||
{
|
||||
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -902,7 +902,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||
{
|
||||
Log.Debug($"[FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -950,8 +950,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
||||
{
|
||||
Log.Info(
|
||||
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,8 +1025,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
if (preInitializers.Count != 0 || initializers.Count != 0)
|
||||
{
|
||||
Log.Info(
|
||||
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1409,8 +1409,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
|
||||
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
||||
Log.Debug(
|
||||
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||
if (mapSize > int.MaxValue)
|
||||
{
|
||||
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
|
||||
@@ -1842,16 +1842,16 @@ public sealed class SelfLoader : ISelfLoader
|
||||
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
|
||||
|
||||
var guestAddr = location + imageBase;
|
||||
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
||||
if (virtualMemory.TryRead(guestAddr, tableBytes))
|
||||
{
|
||||
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (virtualMemory.TryRead(location, tableBytes))
|
||||
{
|
||||
Log.Debug($"TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1859,11 +1859,11 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
var slice = elfData.Slice((int)location, (int)size);
|
||||
tableBytes = slice.ToArray();
|
||||
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
||||
return true;
|
||||
}
|
||||
|
||||
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
|
||||
tableBytes = Array.Empty<byte>();
|
||||
return false;
|
||||
}
|
||||
@@ -1920,17 +1920,17 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
|
||||
{
|
||||
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][TEST] ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
||||
|
||||
if (address == 0)
|
||||
{
|
||||
Log.Debug("-> return 0 (null)");
|
||||
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
|
||||
{
|
||||
Log.Debug($"-> resolved raw 0x{resolved:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> resolved raw 0x{resolved:X16}");
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -1939,18 +1939,18 @@ public sealed class SelfLoader : ISelfLoader
|
||||
var rebased = address + imageBase;
|
||||
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
|
||||
{
|
||||
Log.Debug($"-> resolved rebased 0x{resolvedRebased:X16}");
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> resolved rebased 0x{resolvedRebased:X16}");
|
||||
return resolvedRebased;
|
||||
}
|
||||
}
|
||||
|
||||
if (address < 0x10000)
|
||||
{
|
||||
Log.Debug($"-> reject small 0x{address:X}");
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Log.Debug($"-> fallback raw 0x{address:X}");
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
|
||||
return address;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,11 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Memory;
|
||||
|
||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
||||
|
||||
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||
private readonly object _guestAllocationGate = new();
|
||||
private readonly object _allocationSearchHintGate = new();
|
||||
@@ -58,11 +55,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern void* GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||
|
||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||
{
|
||||
@@ -458,7 +451,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||
{
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||
FlushInstructionCache(null, (void*)address, (nuint)size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -706,7 +699,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||
if (IsExecutableProtection(oldProtect))
|
||||
{
|
||||
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
||||
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1055,7 +1048,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Debug(message);
|
||||
Console.Error.WriteLine($"[VMEM] {message}");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
|
||||
@@ -11,19 +11,15 @@ using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.AppContent;
|
||||
using SharpEmu.Libs.SaveData;
|
||||
using SharpEmu.Logging;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
|
||||
namespace SharpEmu.Core.Runtime;
|
||||
|
||||
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
|
||||
|
||||
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
||||
|
||||
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -132,7 +128,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
||||
Log.Info($"Loading: {ebootPath}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||
LastExecutionDiagnostics = null;
|
||||
LastExecutionTrace = null;
|
||||
LastSessionSummary = null;
|
||||
@@ -142,10 +138,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||
LogAppBundleInfo(normalizedEbootPath, image);
|
||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
||||
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
||||
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
||||
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
||||
@@ -168,7 +163,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName);
|
||||
if (initializerResult is { } failedInitializerResult)
|
||||
{
|
||||
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -176,8 +171,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return failedInitializerResult;
|
||||
}
|
||||
|
||||
Log.Info($"Dispatching, gen: {generation}");
|
||||
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
||||
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
image.EntryPoint,
|
||||
@@ -187,8 +182,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName,
|
||||
_cpuExecutionOptions);
|
||||
|
||||
Log.Info($"DispatchEntry returned: {result}");
|
||||
Log.Info($"Dispatch result: {result}");
|
||||
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -359,66 +354,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
|
||||
{
|
||||
var executableName = Path.GetFileName(ebootPath);
|
||||
if (string.IsNullOrWhiteSpace(executableName))
|
||||
{
|
||||
executableName = "eboot.bin";
|
||||
}
|
||||
|
||||
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
|
||||
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
|
||||
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
|
||||
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("App bundle info:");
|
||||
builder.AppendLine($"- Display name: {displayName}");
|
||||
builder.AppendLine($"- Version: {version}");
|
||||
builder.AppendLine($"- Title ID: {titleId}");
|
||||
builder.AppendLine($"- Content ID: {contentId}");
|
||||
builder.AppendLine($"- Executable: {executableName}");
|
||||
builder.Append("- Platform: PlayStation 5");
|
||||
Log.Info(builder.ToString());
|
||||
}
|
||||
|
||||
private static string? ResolveContentId(string? bundleRoot)
|
||||
{
|
||||
if (string.IsNullOrEmpty(bundleRoot))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var candidate in new[]
|
||||
{
|
||||
Path.Combine(bundleRoot, "sce_sys", "param.json"),
|
||||
Path.Combine(bundleRoot, "param.json"),
|
||||
})
|
||||
{
|
||||
if (!File.Exists(candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
|
||||
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
|
||||
{
|
||||
return contentId.GetString();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
||||
{
|
||||
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
||||
@@ -499,8 +434,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
moduleName = $"module#{i}";
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||
initEntryPoint,
|
||||
@@ -511,8 +446,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
_cpuExecutionOptions);
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
Log.Error(
|
||||
$"Module start failed: {moduleName} -> {result}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -533,8 +468,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return null;
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
|
||||
var result = RunInitializerList(
|
||||
$"{label}:preinit",
|
||||
@@ -573,8 +508,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
continue;
|
||||
}
|
||||
|
||||
Log.Debug(
|
||||
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
initializerAddress,
|
||||
@@ -642,7 +577,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
.ToArray();
|
||||
if (skippedModules.Length > 0)
|
||||
{
|
||||
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||
}
|
||||
|
||||
if (modulePaths.Length == 0)
|
||||
@@ -650,8 +585,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
||||
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
||||
var loadedModules = 0;
|
||||
var failedModules = 0;
|
||||
var mergedImportCount = 0;
|
||||
@@ -686,18 +621,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
||||
loadedModules++;
|
||||
|
||||
Log.Info(
|
||||
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failedModules++;
|
||||
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
|
||||
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
|
||||
}
|
||||
}
|
||||
|
||||
Log.Info(
|
||||
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
@@ -717,8 +652,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (rebound != 0 || unresolved != 0)
|
||||
{
|
||||
Log.Info(
|
||||
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -750,8 +685,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Log.Warning(
|
||||
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -763,8 +698,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Log.Error(
|
||||
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -773,8 +708,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (logRebind)
|
||||
{
|
||||
Log.Debug(
|
||||
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
rebound++;
|
||||
@@ -810,8 +745,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||
{
|
||||
Log.Warning(
|
||||
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -920,8 +855,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
image.EntryPoint,
|
||||
isMain,
|
||||
isSystemModule);
|
||||
Log.Info(
|
||||
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||
}
|
||||
|
||||
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
@@ -14,10 +14,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageReference Include="Iced" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -1,198 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="SharpEmu.GUI.App"
|
||||
RequestedThemeVariant="Dark">
|
||||
|
||||
<Application.Resources>
|
||||
<Color x:Key="SystemAccentColor">#7C5CFC</Color>
|
||||
|
||||
<LinearGradientBrush x:Key="BgBrush" StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0" Color="#12151F" />
|
||||
<GradientStop Offset="0.55" Color="#0D1017" />
|
||||
<GradientStop Offset="1" Color="#0B0D14" />
|
||||
</LinearGradientBrush>
|
||||
|
||||
<SolidColorBrush x:Key="ChromeBrush" Color="#090C12" />
|
||||
<SolidColorBrush x:Key="CardBrush" Color="#141924" />
|
||||
<SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" />
|
||||
<SolidColorBrush x:Key="ElevatedBrush" Color="#1B2230" />
|
||||
<SolidColorBrush x:Key="TextBrush" Color="#E8ECF4" />
|
||||
<SolidColorBrush x:Key="MutedBrush" Color="#8B94A7" />
|
||||
<SolidColorBrush x:Key="FaintBrush" Color="#5A6478" />
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="#7C5CFC" />
|
||||
<SolidColorBrush x:Key="AccentHoverBrush" Color="#8F73FF" />
|
||||
<SolidColorBrush x:Key="DangerBrush" Color="#E5484D" />
|
||||
<SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" />
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
|
||||
<SolidColorBrush x:Key="TileHoverBrush" Color="#1A2130" />
|
||||
<SolidColorBrush x:Key="TileSelectedBrush" Color="#212A3F" />
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
|
||||
<Style Selector="Window">
|
||||
<Setter Property="FontFamily" Value="Inter, Segoe UI, sans-serif" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.card">
|
||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="Padding" Value="16" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.pill">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="CornerRadius" Value="999" />
|
||||
<Setter Property="Padding" Value="10,3" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.sectionTitle">
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="LetterSpacing" Value="1.5" />
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.fieldLabel">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
|
||||
<Setter Property="Margin" Value="0,0,0,6" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.accent">
|
||||
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Padding" Value="22,10" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource AccentHoverBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.danger">
|
||||
<Setter Property="Background" Value="{StaticResource DangerBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Padding" Value="22,10" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.danger:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource DangerHoverBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.ghost">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="Padding" Value="12,7" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ToggleButton.ghost">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="Padding" Value="12,7" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="ToggleButton.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="ToggleButton.ghost:checked /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ContextMenu">
|
||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="Padding" Value="6" />
|
||||
</Style>
|
||||
<Style Selector="ContextMenu MenuItem">
|
||||
<Setter Property="Padding" Value="10,7" />
|
||||
<Setter Property="CornerRadius" Value="7" />
|
||||
</Style>
|
||||
<Style Selector="ContextMenu Separator">
|
||||
<Setter Property="Background" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="Height" Value="1" />
|
||||
<Setter Property="Margin" Value="8,4" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ListBox.console">
|
||||
<Setter Property="Background" Value="#0B0E14" />
|
||||
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.console ListBoxItem">
|
||||
<Setter Property="Padding" Value="10,1" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
</Style>
|
||||
|
||||
<!-- Cover-art library grid -->
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem">
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<Setter Property="Margin" Value="5" />
|
||||
<Setter Property="CornerRadius" Value="14" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="RenderTransform" Value="translateY(0px)" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" />
|
||||
</Transitions>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover">
|
||||
<Setter Property="RenderTransform" Value="translateY(-3px)" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource TileHoverBrush}" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentHoverBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.coverShadow">
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="BoxShadow" Value="0 6 14 0 #55000000" />
|
||||
</Style>
|
||||
<Style Selector="Border.coverClip">
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="ClipToBounds" Value="True" />
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
</Application>
|
||||
@@ -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,120 +0,0 @@
|
||||
#nullable disable
|
||||
using System.IO;
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the configuration data needed to decode or encode an ATRAC9 stream.
|
||||
/// </summary>
|
||||
public class Atrac9Config
|
||||
{
|
||||
/// <summary>
|
||||
/// The 4-byte ATRAC9 configuration data.
|
||||
/// </summary>
|
||||
public byte[] ConfigData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A 4-bit value specifying one of 16 sample rates.
|
||||
/// </summary>
|
||||
public int SampleRateIndex { get; }
|
||||
/// <summary>
|
||||
/// A 3-bit value specifying one of 6 substream channel mappings.
|
||||
/// </summary>
|
||||
public int ChannelConfigIndex { get; }
|
||||
/// <summary>
|
||||
/// An 11-bit value containing the average size of a single frame.
|
||||
/// </summary>
|
||||
public int FrameBytes { get; }
|
||||
/// <summary>
|
||||
/// A 2-bit value indicating how many frames are in each superframe.
|
||||
/// </summary>
|
||||
public int SuperframeIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The channel mapping used by the ATRAC9 stream.
|
||||
/// </summary>
|
||||
public ChannelConfig ChannelConfig { get; }
|
||||
/// <summary>
|
||||
/// The total number of channels in the ATRAC9 stream.
|
||||
/// </summary>
|
||||
public int ChannelCount { get; }
|
||||
/// <summary>
|
||||
/// The sample rate of the ATRAC9 stream.
|
||||
/// </summary>
|
||||
public int SampleRate { get; }
|
||||
/// <summary>
|
||||
/// Indicates whether the ATRAC9 stream has a <see cref="SampleRateIndex"/> of 8 or above.
|
||||
/// </summary>
|
||||
public bool HighSampleRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of frames in each superframe.
|
||||
/// </summary>
|
||||
public int FramesPerSuperframe { get; }
|
||||
/// <summary>
|
||||
/// The number of samples in one frame as an exponent of 2.
|
||||
/// <see cref="FrameSamples"/> = 2^<see cref="FrameSamplesPower"/>.
|
||||
/// </summary>
|
||||
public int FrameSamplesPower { get; }
|
||||
/// <summary>
|
||||
/// The number of samples in one frame.
|
||||
/// </summary>
|
||||
public int FrameSamples { get; }
|
||||
/// <summary>
|
||||
/// The number of bytes in one superframe.
|
||||
/// </summary>
|
||||
public int SuperframeBytes { get; }
|
||||
/// <summary>
|
||||
/// The number of samples in one superframe.
|
||||
/// </summary>
|
||||
public int SuperframeSamples { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Reads ATRAC9 configuration data and calculates the stream parameters from it.
|
||||
/// </summary>
|
||||
/// <param name="configData">The processed ATRAC9 configuration.</param>
|
||||
public Atrac9Config(byte[] configData)
|
||||
{
|
||||
if (configData == null || configData.Length != 4)
|
||||
{
|
||||
throw new InvalidDataException("Config data must be 4 bytes long");
|
||||
}
|
||||
|
||||
ReadConfigData(configData, out int a, out int b, out int c, out int d);
|
||||
SampleRateIndex = a;
|
||||
ChannelConfigIndex = b;
|
||||
FrameBytes = c;
|
||||
SuperframeIndex = d;
|
||||
ConfigData = configData;
|
||||
|
||||
FramesPerSuperframe = 1 << SuperframeIndex;
|
||||
SuperframeBytes = FrameBytes << SuperframeIndex;
|
||||
ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex];
|
||||
|
||||
ChannelCount = ChannelConfig.ChannelCount;
|
||||
SampleRate = Tables.SampleRates[SampleRateIndex];
|
||||
HighSampleRate = SampleRateIndex > 7;
|
||||
FrameSamplesPower = Tables.SamplingRateIndexToFrameSamplesPower[SampleRateIndex];
|
||||
FrameSamples = 1 << FrameSamplesPower;
|
||||
SuperframeSamples = FrameSamples * FramesPerSuperframe;
|
||||
}
|
||||
|
||||
private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex)
|
||||
{
|
||||
var reader = new BitReader(configData);
|
||||
|
||||
int header = reader.ReadInt(8);
|
||||
sampleRateIndex = reader.ReadInt(4);
|
||||
channelConfigIndex = reader.ReadInt(3);
|
||||
int validationBit = reader.ReadInt(1);
|
||||
frameBytes = reader.ReadInt(11) + 1;
|
||||
superframeIndex = reader.ReadInt(2);
|
||||
|
||||
if (header != 0xFE || validationBit != 0)
|
||||
{
|
||||
throw new InvalidDataException("ATRAC9 Config Data is invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,128 +0,0 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
/// <summary>
|
||||
/// Decodes an ATRAC9 stream into 16-bit PCM.
|
||||
/// </summary>
|
||||
public class Atrac9Decoder
|
||||
{
|
||||
/// <summary>
|
||||
/// The config data for the current ATRAC9 stream.
|
||||
/// </summary>
|
||||
public Atrac9Config Config { get; private set; }
|
||||
|
||||
private Frame Frame { get; set; }
|
||||
private BitReader Reader { get; set; }
|
||||
private bool _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the decoder to decode an ATRAC9 stream based on the information in <paramref name="configData"/>.
|
||||
/// </summary>
|
||||
/// <param name="configData">A 4-byte value containing information about the ATRAC9 stream.</param>
|
||||
public void Initialize(byte[] configData)
|
||||
{
|
||||
Config = new Atrac9Config(configData);
|
||||
Frame = new Frame(Config);
|
||||
Reader = new BitReader(null);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes one superframe of ATRAC9 data.
|
||||
/// </summary>
|
||||
/// <param name="atrac9Data">The ATRAC9 data to decode. The array must be at least
|
||||
/// <see cref="Config"/>.<see cref="Atrac9Config.SuperframeBytes"/> bytes long.</param>
|
||||
/// <param name="pcmOut">A buffer that the decoded PCM data will be placed in.
|
||||
/// The array must have dimensions of at least [<see cref="Config"/>.<see cref="Atrac9Config.ChannelCount"/>]
|
||||
/// [<see cref="Config"/>.<see cref="Atrac9Config.SuperframeSamples"/>].</param>
|
||||
public void Decode(byte[] atrac9Data, short[][] pcmOut)
|
||||
{
|
||||
if (!_initialized) throw new InvalidOperationException("Decoder must be initialized before decoding.");
|
||||
|
||||
ValidateDecodeBuffers(atrac9Data, pcmOut);
|
||||
Reader.SetBuffer(atrac9Data);
|
||||
DecodeSuperFrame(pcmOut);
|
||||
}
|
||||
|
||||
private void ValidateDecodeBuffers(byte[] atrac9Buffer, short[][] pcmBuffer)
|
||||
{
|
||||
if (atrac9Buffer == null) throw new ArgumentNullException(nameof(atrac9Buffer));
|
||||
if (pcmBuffer == null) throw new ArgumentNullException(nameof(pcmBuffer));
|
||||
|
||||
if (atrac9Buffer.Length < Config.SuperframeBytes)
|
||||
{
|
||||
throw new ArgumentException("ATRAC9 buffer is too small");
|
||||
}
|
||||
|
||||
if (pcmBuffer.Length < Config.ChannelCount)
|
||||
{
|
||||
throw new ArgumentException("PCM buffer is too small");
|
||||
}
|
||||
|
||||
for (int i = 0; i < Config.ChannelCount; i++)
|
||||
{
|
||||
if (pcmBuffer[i]?.Length < Config.SuperframeSamples)
|
||||
{
|
||||
throw new ArgumentException("PCM buffer is too small");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DecodeSuperFrame(short[][] pcmOut)
|
||||
{
|
||||
for (int i = 0; i < Config.FramesPerSuperframe; i++)
|
||||
{
|
||||
Frame.FrameIndex = i;
|
||||
DecodeFrame(Reader, Frame);
|
||||
PcmFloatToShort(pcmOut, i * Config.FrameSamples);
|
||||
Reader.AlignPosition(8);
|
||||
}
|
||||
}
|
||||
|
||||
private void PcmFloatToShort(short[][] pcmOut, int start)
|
||||
{
|
||||
int endSample = start + Config.FrameSamples;
|
||||
int channelNum = 0;
|
||||
foreach (Block block in Frame.Blocks)
|
||||
{
|
||||
foreach (Channel channel in block.Channels)
|
||||
{
|
||||
double[] pcmSrc = channel.Pcm;
|
||||
short[] pcmDest = pcmOut[channelNum++];
|
||||
for (int d = 0, s = start; s < endSample; d++, s++)
|
||||
{
|
||||
double sample = pcmSrc[d];
|
||||
// Not using Math.Round because it's ~20x slower on 64-bit
|
||||
int roundedSample = (int)Math.Floor(sample + 0.5);
|
||||
pcmDest[s] = Helpers.Clamp16(roundedSample);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DecodeFrame(BitReader reader, Frame frame)
|
||||
{
|
||||
Unpack.UnpackFrame(reader, frame);
|
||||
|
||||
foreach (Block block in frame.Blocks)
|
||||
{
|
||||
Quantization.DequantizeSpectra(block);
|
||||
Stereo.ApplyIntensityStereo(block);
|
||||
Quantization.ScaleSpectrum(block);
|
||||
BandExtension.ApplyBandExtension(block);
|
||||
ImdctBlock(block);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ImdctBlock(Block block)
|
||||
{
|
||||
foreach (Channel channel in block.Channels)
|
||||
{
|
||||
channel.Mdct.RunImdct(channel.Spectra, channel.Pcm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9
|
||||
{
|
||||
/// <summary>
|
||||
/// An Xorshift RNG used by the ATRAC9 codec
|
||||
/// </summary>
|
||||
internal class Atrac9Rng
|
||||
{
|
||||
private ushort _stateA;
|
||||
private ushort _stateB;
|
||||
private ushort _stateC;
|
||||
private ushort _stateD;
|
||||
|
||||
public Atrac9Rng(ushort seed)
|
||||
{
|
||||
int startValue = 0x4D93 * (seed ^ (seed >> 14));
|
||||
|
||||
_stateA = (ushort)(3 - startValue);
|
||||
_stateB = (ushort)(2 - startValue);
|
||||
_stateC = (ushort)(1 - startValue);
|
||||
_stateD = (ushort)(0 - startValue);
|
||||
}
|
||||
|
||||
public ushort Next()
|
||||
{
|
||||
ushort t = (ushort)(_stateD ^ (_stateD << 5));
|
||||
_stateD = _stateC;
|
||||
_stateC = _stateB;
|
||||
_stateB = _stateA;
|
||||
_stateA = (ushort)(t ^ _stateA ^ ((t ^ (_stateA >> 5)) >> 4));
|
||||
return _stateA;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,373 +0,0 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class BandExtension
|
||||
{
|
||||
public static void ApplyBandExtension(Block block)
|
||||
{
|
||||
if (!block.BandExtensionEnabled || !block.HasExtensionData) return;
|
||||
|
||||
foreach (Channel channel in block.Channels)
|
||||
{
|
||||
ApplyBandExtensionChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyBandExtensionChannel(Channel channel)
|
||||
{
|
||||
int groupAUnit = channel.Block.QuantizationUnitCount;
|
||||
int[] scaleFactors = channel.ScaleFactors;
|
||||
double[] spectra = channel.Spectra;
|
||||
double[] scales = channel.BexScales;
|
||||
int[] values = channel.BexValues;
|
||||
|
||||
GetBexBandInfo(out int bandCount, out int groupBUnit, out int groupCUnit, groupAUnit);
|
||||
int totalUnits = Math.Max(groupCUnit, 22);
|
||||
|
||||
int groupABin = Tables.QuantUnitToCoeffIndex[groupAUnit];
|
||||
int groupBBin = Tables.QuantUnitToCoeffIndex[groupBUnit];
|
||||
int groupCBin = Tables.QuantUnitToCoeffIndex[groupCUnit];
|
||||
int totalBins = Tables.QuantUnitToCoeffIndex[totalUnits];
|
||||
|
||||
FillHighFrequencies(spectra, groupABin, groupBBin, groupCBin, totalBins);
|
||||
|
||||
switch (channel.BexMode)
|
||||
{
|
||||
case 0:
|
||||
int bexQuantUnits = totalUnits - groupAUnit;
|
||||
|
||||
switch (bandCount)
|
||||
{
|
||||
case 3:
|
||||
scales[0] = BexMode0Bands3[0][values[0]];
|
||||
scales[1] = BexMode0Bands3[1][values[0]];
|
||||
scales[2] = BexMode0Bands3[2][values[1]];
|
||||
scales[3] = BexMode0Bands3[3][values[2]];
|
||||
scales[4] = BexMode0Bands3[4][values[3]];
|
||||
break;
|
||||
case 4:
|
||||
scales[0] = BexMode0Bands4[0][values[0]];
|
||||
scales[1] = BexMode0Bands4[1][values[0]];
|
||||
scales[2] = BexMode0Bands4[2][values[1]];
|
||||
scales[3] = BexMode0Bands4[3][values[2]];
|
||||
scales[4] = BexMode0Bands4[4][values[3]];
|
||||
break;
|
||||
case 5:
|
||||
scales[0] = BexMode0Bands5[0][values[0]];
|
||||
scales[1] = BexMode0Bands5[1][values[1]];
|
||||
scales[2] = BexMode0Bands5[2][values[1]];
|
||||
break;
|
||||
}
|
||||
|
||||
scales[bexQuantUnits - 1] = Tables.SpectrumScale[scaleFactors[groupAUnit]];
|
||||
|
||||
AddNoiseToSpectrum(channel, Tables.QuantUnitToCoeffIndex[totalUnits - 1],
|
||||
Tables.QuantUnitToCoeffCount[totalUnits - 1]);
|
||||
ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits);
|
||||
break;
|
||||
case 1:
|
||||
for (int i = groupAUnit; i < totalUnits; i++)
|
||||
{
|
||||
scales[i - groupAUnit] = Tables.SpectrumScale[scaleFactors[i]];
|
||||
}
|
||||
|
||||
AddNoiseToSpectrum(channel, groupABin, totalBins - groupABin);
|
||||
ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits);
|
||||
break;
|
||||
case 2:
|
||||
double groupAScale2 = BexMode2Scale[values[0]];
|
||||
double groupBScale2 = BexMode2Scale[values[1]];
|
||||
|
||||
for (int i = groupABin; i < groupBBin; i++)
|
||||
{
|
||||
spectra[i] *= groupAScale2;
|
||||
}
|
||||
|
||||
for (int i = groupBBin; i < groupCBin; i++)
|
||||
{
|
||||
spectra[i] *= groupBScale2;
|
||||
}
|
||||
return;
|
||||
case 3:
|
||||
double rate = Math.Pow(2, BexMode3Rate[values[1]]);
|
||||
double scale = BexMode3Initial[values[0]];
|
||||
for (int i = groupABin; i < totalBins; i++)
|
||||
{
|
||||
scale *= rate;
|
||||
spectra[i] *= scale;
|
||||
}
|
||||
return;
|
||||
case 4:
|
||||
double mult = BexMode4Multiplier[values[0]];
|
||||
double groupAScale4 = 0.7079468 * mult;
|
||||
double groupBScale4 = 0.5011902 * mult;
|
||||
double groupCScale4 = 0.3548279 * mult;
|
||||
|
||||
for (int i = groupABin; i < groupBBin; i++)
|
||||
{
|
||||
spectra[i] *= groupAScale4;
|
||||
}
|
||||
|
||||
for (int i = groupBBin; i < groupCBin; i++)
|
||||
{
|
||||
spectra[i] *= groupBScale4;
|
||||
}
|
||||
|
||||
for (int i = groupCBin; i < totalBins; i++)
|
||||
{
|
||||
spectra[i] *= groupCScale4;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ScaleBexQuantUnits(double[] spectra, double[] scales, int startUnit, int totalUnits)
|
||||
{
|
||||
for (int i = startUnit; i < totalUnits; i++)
|
||||
{
|
||||
for (int k = Tables.QuantUnitToCoeffIndex[i]; k < Tables.QuantUnitToCoeffIndex[i + 1]; k++)
|
||||
{
|
||||
spectra[k] *= scales[i - startUnit];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillHighFrequencies(double[] spectra, int groupABin, int groupBBin, int groupCBin, int totalBins)
|
||||
{
|
||||
for (int i = 0; i < groupBBin - groupABin; i++)
|
||||
{
|
||||
spectra[groupABin + i] = spectra[groupABin - i - 1];
|
||||
}
|
||||
|
||||
for (int i = 0; i < groupCBin - groupBBin; i++)
|
||||
{
|
||||
spectra[groupBBin + i] = spectra[groupBBin - i - 1];
|
||||
}
|
||||
|
||||
for (int i = 0; i < totalBins - groupCBin; i++)
|
||||
{
|
||||
spectra[groupCBin + i] = spectra[groupCBin - i - 1];
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddNoiseToSpectrum(Channel channel, int index, int count)
|
||||
{
|
||||
if (channel.Rng == null)
|
||||
{
|
||||
int[] sf = channel.ScaleFactors;
|
||||
ushort seed = (ushort)(543 * (sf[8] + sf[12] + sf[15] + 1));
|
||||
channel.Rng = new Atrac9Rng(seed);
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
channel.Spectra[i + index] = channel.Rng.Next() / 65535.0 * 2.0 - 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetBexBandInfo(out int bandCount, out int groupAUnit, out int groupBUnit, int quantUnits)
|
||||
{
|
||||
groupAUnit = BexGroupInfo[quantUnits - 13][0];
|
||||
groupBUnit = BexGroupInfo[quantUnits - 13][1];
|
||||
bandCount = BexGroupInfo[quantUnits - 13][2];
|
||||
}
|
||||
|
||||
public static readonly byte[][] BexGroupInfo =
|
||||
{
|
||||
new byte[] {16, 21, 0},
|
||||
new byte[] {18, 22, 1},
|
||||
new byte[] {20, 22, 2},
|
||||
new byte[] {21, 22, 3},
|
||||
new byte[] {21, 22, 3},
|
||||
new byte[] {23, 24, 4},
|
||||
new byte[] {23, 24, 4},
|
||||
new byte[] {24, 24, 5}
|
||||
};
|
||||
|
||||
// [mode][bands]
|
||||
|
||||
public static readonly byte[][] BexEncodedValueCounts =
|
||||
{
|
||||
new byte[] {0, 0, 0, 4, 4, 2},
|
||||
new byte[] {0, 0, 0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 2, 2, 1},
|
||||
new byte[] {0, 0, 0, 2, 2, 2},
|
||||
new byte[] {1, 1, 1, 0, 0, 0}
|
||||
};
|
||||
|
||||
// [mode][bands][valueIndex]
|
||||
|
||||
public static readonly byte[][][] BexDataLengths =
|
||||
{
|
||||
new[] {
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {5, 4, 3, 3},
|
||||
new byte[] {4, 4, 3, 4},
|
||||
new byte[] {4, 5, 0, 0}
|
||||
}, new[] {
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0}
|
||||
}, new[] {
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {6, 6, 0, 0},
|
||||
new byte[] {6, 6, 0, 0},
|
||||
new byte[] {6, 0, 0, 0}
|
||||
}, new[] {
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {4, 4, 0, 0},
|
||||
new byte[] {4, 4, 0, 0},
|
||||
new byte[] {4, 4, 0, 0}
|
||||
}, new[] {
|
||||
new byte[] {3, 0, 0, 0},
|
||||
new byte[] {3, 0, 0, 0},
|
||||
new byte[] {3, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0}
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly double[][] BexMode0Bands3 =
|
||||
{
|
||||
new[] {
|
||||
0.000000e+0, 1.988220e-1, 2.514343e-1, 2.960510e-1,
|
||||
3.263550e-1, 3.771362e-1, 3.786926e-1, 4.540405e-1,
|
||||
4.877625e-1, 5.262451e-1, 5.447083e-1, 5.737000e-1,
|
||||
6.212158e-1, 6.222839e-1, 6.560974e-1, 6.896667e-1,
|
||||
7.555542e-1, 7.677917e-1, 7.918091e-1, 7.971497e-1,
|
||||
8.188171e-1, 8.446045e-1, 9.790649e-1, 9.822083e-1,
|
||||
9.846191e-1, 9.859314e-1, 9.863586e-1, 9.863892e-1,
|
||||
9.873352e-1, 9.881287e-1, 9.898682e-1, 9.913330e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 9.982910e-1, 7.592773e-2, 7.179565e-1,
|
||||
9.851379e-1, 5.340271e-1, 9.013672e-1, 6.349182e-1,
|
||||
7.226257e-1, 1.948547e-1, 7.628174e-1, 9.873657e-1,
|
||||
8.112183e-1, 2.715454e-1, 9.734192e-1, 1.443787e-1,
|
||||
4.640198e-1, 3.249207e-1, 3.790894e-1, 8.276367e-2,
|
||||
5.954590e-1, 2.864380e-1, 9.806824e-1, 7.929077e-1,
|
||||
6.292114e-1, 4.887085e-1, 2.905273e-1, 1.301880e-1,
|
||||
3.140869e-1, 5.482483e-1, 4.210815e-1, 1.182861e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 3.155518e-2, 8.581543e-2, 1.364746e-1,
|
||||
1.858826e-1, 2.368469e-1, 2.888184e-1, 3.432617e-1,
|
||||
4.012451e-1, 4.623108e-1, 5.271301e-1, 5.954895e-1,
|
||||
6.681213e-1, 7.448425e-1, 8.245239e-1, 9.097290e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 4.418945e-2, 1.303711e-1, 2.273560e-1,
|
||||
3.395996e-1, 4.735718e-1, 6.267090e-1, 8.003845e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 2.804565e-2, 9.683228e-2, 1.849976e-1,
|
||||
3.005981e-1, 4.470520e-1, 6.168518e-1, 8.007813e-1
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly double[][] BexMode0Bands4 =
|
||||
{
|
||||
new[] {
|
||||
0.000000e+0, 2.708740e-1, 3.479614e-1, 3.578186e-1,
|
||||
5.083618e-1, 5.299072e-1, 5.819092e-1, 6.381836e-1,
|
||||
7.276917e-1, 7.595520e-1, 7.878723e-1, 9.707336e-1,
|
||||
9.713135e-1, 9.736023e-1, 9.759827e-1, 9.832458e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 2.330627e-1, 5.891418e-1, 7.170410e-1,
|
||||
2.036438e-1, 1.613464e-1, 6.668701e-1, 9.481201e-1,
|
||||
9.769897e-1, 5.111694e-1, 3.522644e-1, 8.209534e-1,
|
||||
2.933960e-1, 9.757690e-1, 5.289917e-1, 4.372253e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 4.360962e-2, 1.056519e-1, 1.590576e-1,
|
||||
2.078857e-1, 2.572937e-1, 3.082581e-1, 3.616028e-1,
|
||||
4.191589e-1, 4.792175e-1, 5.438538e-1, 6.125183e-1,
|
||||
6.841125e-1, 7.589417e-1, 8.365173e-1, 9.148254e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 4.074097e-2, 1.164551e-1, 2.077026e-1,
|
||||
3.184509e-1, 4.532166e-1, 6.124268e-1, 7.932129e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 8.880615e-3, 2.932739e-2, 5.593872e-2,
|
||||
8.825684e-2, 1.259155e-1, 1.721497e-1, 2.270813e-1,
|
||||
2.901611e-1, 3.579712e-1, 4.334106e-1, 5.147095e-1,
|
||||
6.023254e-1, 6.956177e-1, 7.952881e-1, 8.977356e-1
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly double[][] BexMode0Bands5 =
|
||||
{
|
||||
new[] {
|
||||
0.000000e+0, 7.379150e-2, 1.806335e-1, 2.687073e-1,
|
||||
3.407898e-1, 4.047546e-1, 4.621887e-1, 5.168762e-1,
|
||||
5.703125e-1, 6.237488e-1, 6.763611e-1, 7.288208e-1,
|
||||
7.808533e-1, 8.337708e-1, 8.874512e-1, 9.418030e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 7.980347e-2, 1.615295e-1, 1.665649e-1,
|
||||
1.822205e-1, 2.185669e-1, 2.292175e-1, 2.456665e-1,
|
||||
2.666321e-1, 3.306580e-1, 3.330688e-1, 3.765259e-1,
|
||||
4.085083e-1, 4.400024e-1, 4.407654e-1, 4.817505e-1,
|
||||
4.924011e-1, 5.320740e-1, 5.893860e-1, 6.131287e-1,
|
||||
6.212463e-1, 6.278076e-1, 6.308899e-1, 7.660828e-1,
|
||||
7.850647e-1, 7.910461e-1, 7.929382e-1, 8.038330e-1,
|
||||
9.834900e-1, 9.846191e-1, 9.852295e-1, 9.862671e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 6.084290e-1, 3.672791e-1, 3.151855e-1,
|
||||
1.488953e-1, 2.571716e-1, 5.103455e-1, 3.311157e-1,
|
||||
5.426025e-2, 4.254456e-1, 7.998352e-1, 7.873230e-1,
|
||||
5.418701e-1, 2.925110e-1, 8.468628e-2, 1.410522e-1,
|
||||
9.819641e-1, 9.609070e-1, 3.530884e-2, 9.729004e-2,
|
||||
5.758362e-1, 9.941711e-1, 7.215576e-1, 7.183228e-1,
|
||||
2.028809e-1, 9.588623e-2, 2.032166e-1, 1.338806e-1,
|
||||
5.003357e-1, 1.874390e-1, 9.804993e-1, 1.107788e-1
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly double[] BexMode2Scale =
|
||||
{
|
||||
4.272461e-4, 1.312256e-3, 2.441406e-3, 3.692627e-3,
|
||||
4.913330e-3, 6.134033e-3, 7.507324e-3, 8.972168e-3,
|
||||
1.049805e-2, 1.223755e-2, 1.406860e-2, 1.599121e-2,
|
||||
1.800537e-2, 2.026367e-2, 2.264404e-2, 2.517700e-2,
|
||||
2.792358e-2, 3.073120e-2, 3.344727e-2, 3.631592e-2,
|
||||
3.952026e-2, 4.275513e-2, 4.608154e-2, 4.968262e-2,
|
||||
5.355835e-2, 5.783081e-2, 6.195068e-2, 6.677246e-2,
|
||||
7.196045e-2, 7.745361e-2, 8.319092e-2, 8.993530e-2,
|
||||
9.759521e-2, 1.056213e-1, 1.138916e-1, 1.236267e-1,
|
||||
1.348267e-1, 1.470337e-1, 1.603394e-1, 1.755676e-1,
|
||||
1.905823e-1, 2.071228e-1, 2.245178e-1, 2.444153e-1,
|
||||
2.658997e-1, 2.897644e-1, 3.146057e-1, 3.450012e-1,
|
||||
3.766174e-1, 4.122620e-1, 4.505615e-1, 4.893799e-1,
|
||||
5.305481e-1, 5.731201e-1, 6.157837e-1, 6.580811e-1,
|
||||
6.985168e-1, 7.435303e-1, 7.865906e-1, 8.302612e-1,
|
||||
8.718567e-1, 9.125671e-1, 9.575806e-1, 9.996643e-1
|
||||
};
|
||||
|
||||
public static readonly double[] BexMode3Initial =
|
||||
{
|
||||
3.491211e-1, 5.371094e-1, 6.782227e-1, 7.910156e-1,
|
||||
9.057617e-1, 1.024902e+0, 1.156250e+0, 1.290527e+0,
|
||||
1.458984e+0, 1.664551e+0, 1.929688e+0, 2.278320e+0,
|
||||
2.831543e+0, 3.659180e+0, 5.257813e+0, 8.373047e+0
|
||||
};
|
||||
|
||||
public static readonly double[] BexMode3Rate =
|
||||
{
|
||||
-2.913818e-1, -2.541504e-1, -1.664429e-1, -1.476440e-1,
|
||||
-1.342163e-1, -1.220703e-1, -1.117554e-1, -1.026611e-1,
|
||||
-9.436035e-2, -8.483887e-2, -7.476807e-2, -6.304932e-2,
|
||||
-4.492188e-2, -2.447510e-2, +1.831055e-4, +4.174805e-2
|
||||
};
|
||||
|
||||
public static readonly double[] BexMode4Multiplier =
|
||||
{
|
||||
3.610229e-2, 1.260681e-1, 2.227478e-1, 3.338318e-1,
|
||||
4.662170e-1, 6.221313e-1, 7.989197e-1, 9.939575e-1
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class BitAllocation
|
||||
{
|
||||
public static void CreateGradient(Block block)
|
||||
{
|
||||
int valueCount = block.GradientEndValue - block.GradientStartValue;
|
||||
int unitCount = block.GradientEndUnit - block.GradientStartUnit;
|
||||
|
||||
for (int i = 0; i < block.GradientEndUnit; i++)
|
||||
{
|
||||
block.Gradient[i] = block.GradientStartValue;
|
||||
}
|
||||
|
||||
for (int i = block.GradientEndUnit; i <= block.QuantizationUnitCount; i++)
|
||||
{
|
||||
block.Gradient[i] = block.GradientEndValue;
|
||||
}
|
||||
if (unitCount <= 0) return;
|
||||
if (valueCount == 0) return;
|
||||
|
||||
byte[] curve = Tables.GradientCurves[unitCount - 1];
|
||||
if (valueCount <= 0)
|
||||
{
|
||||
double scale = (-valueCount - 1) / 31.0;
|
||||
int baseVal = block.GradientStartValue - 1;
|
||||
for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++)
|
||||
{
|
||||
block.Gradient[i] = baseVal - (int)(curve[i - block.GradientStartUnit] * scale);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
double scale = (valueCount - 1) / 31.0;
|
||||
int baseVal = block.GradientStartValue + 1;
|
||||
for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++)
|
||||
{
|
||||
block.Gradient[i] = baseVal + (int)(curve[i - block.GradientStartUnit] * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CalculateMask(Channel channel)
|
||||
{
|
||||
Array.Clear(channel.PrecisionMask, 0, channel.PrecisionMask.Length);
|
||||
for (int i = 1; i < channel.Block.QuantizationUnitCount; i++)
|
||||
{
|
||||
int delta = channel.ScaleFactors[i] - channel.ScaleFactors[i - 1];
|
||||
if (delta > 1)
|
||||
{
|
||||
channel.PrecisionMask[i] += Math.Min(delta - 1, 5);
|
||||
}
|
||||
else if (delta < -1)
|
||||
{
|
||||
channel.PrecisionMask[i - 1] += Math.Min(delta * -1 - 1, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CalculatePrecisions(Channel channel)
|
||||
{
|
||||
Block block = channel.Block;
|
||||
|
||||
if (block.GradientMode != 0)
|
||||
{
|
||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
channel.Precisions[i] = channel.ScaleFactors[i] + channel.PrecisionMask[i] - block.Gradient[i];
|
||||
if (channel.Precisions[i] > 0)
|
||||
{
|
||||
switch (block.GradientMode)
|
||||
{
|
||||
case 1:
|
||||
channel.Precisions[i] /= 2;
|
||||
break;
|
||||
case 2:
|
||||
channel.Precisions[i] = 3 * channel.Precisions[i] / 8;
|
||||
break;
|
||||
case 3:
|
||||
channel.Precisions[i] /= 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
channel.Precisions[i] = channel.ScaleFactors[i] - block.Gradient[i];
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
if (channel.Precisions[i] < 1)
|
||||
{
|
||||
channel.Precisions[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < block.GradientBoundary; i++)
|
||||
{
|
||||
channel.Precisions[i]++;
|
||||
}
|
||||
|
||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
channel.PrecisionsFine[i] = 0;
|
||||
if (channel.Precisions[i] > 15)
|
||||
{
|
||||
channel.PrecisionsFine[i] = channel.Precisions[i] - 15;
|
||||
channel.Precisions[i] = 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[][] GenerateGradientCurves()
|
||||
{
|
||||
byte[] main =
|
||||
{
|
||||
01, 01, 01, 01, 02, 02, 02, 02, 03, 03, 03, 04, 04, 05, 05, 06, 07, 08, 09, 10, 11, 12, 13, 15,
|
||||
16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 26, 27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30
|
||||
};
|
||||
var curves = new byte[main.Length][];
|
||||
|
||||
for (int length = 1; length <= main.Length; length++)
|
||||
{
|
||||
curves[length - 1] = new byte[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
curves[length - 1][i] = main[i * main.Length / length];
|
||||
}
|
||||
}
|
||||
return curves;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,92 +0,0 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal class Block
|
||||
{
|
||||
public Atrac9Config Config { get; }
|
||||
public BlockType BlockType { get; }
|
||||
public int BlockIndex { get; }
|
||||
public Frame Frame { get; }
|
||||
|
||||
public Channel[] Channels { get; }
|
||||
public int ChannelCount { get; }
|
||||
|
||||
public bool FirstInSuperframe { get; set; }
|
||||
public bool ReuseBandParams { get; set; }
|
||||
|
||||
public int BandCount { get; set; }
|
||||
public int StereoBand { get; set; }
|
||||
public int ExtensionBand { get; set; }
|
||||
public int QuantizationUnitCount { get; set; }
|
||||
public int StereoQuantizationUnit { get; set; }
|
||||
public int ExtensionUnit { get; set; }
|
||||
public int QuantizationUnitsPrev { get; set; }
|
||||
|
||||
public int[] Gradient { get; } = new int[31];
|
||||
public int GradientMode { get; set; }
|
||||
public int GradientStartUnit { get; set; }
|
||||
public int GradientStartValue { get; set; }
|
||||
public int GradientEndUnit { get; set; }
|
||||
public int GradientEndValue { get; set; }
|
||||
public int GradientBoundary { get; set; }
|
||||
|
||||
public int PrimaryChannelIndex { get; set; }
|
||||
public int[] JointStereoSigns { get; } = new int[30];
|
||||
public bool HasJointStereoSigns { get; set; }
|
||||
public Channel PrimaryChannel => Channels[PrimaryChannelIndex == 0 ? 0 : 1];
|
||||
public Channel SecondaryChannel => Channels[PrimaryChannelIndex == 0 ? 1 : 0];
|
||||
|
||||
public bool BandExtensionEnabled { get; set; }
|
||||
public bool HasExtensionData { get; set; }
|
||||
public int BexDataLength { get; set; }
|
||||
public int BexMode { get; set; }
|
||||
|
||||
public Block(Frame parentFrame, int blockIndex)
|
||||
{
|
||||
Frame = parentFrame;
|
||||
BlockIndex = blockIndex;
|
||||
Config = parentFrame.Config;
|
||||
BlockType = Config.ChannelConfig.BlockTypes[blockIndex];
|
||||
ChannelCount = BlockTypeToChannelCount(BlockType);
|
||||
Channels = new Channel[ChannelCount];
|
||||
for (int i = 0; i < ChannelCount; i++)
|
||||
{
|
||||
Channels[i] = new Channel(this, i);
|
||||
}
|
||||
}
|
||||
|
||||
public static int BlockTypeToChannelCount(BlockType blockType)
|
||||
{
|
||||
switch (blockType)
|
||||
{
|
||||
case BlockType.Mono:
|
||||
return 1;
|
||||
case BlockType.Stereo:
|
||||
return 2;
|
||||
case BlockType.LFE:
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An ATRAC9 block (substream) type
|
||||
/// </summary>
|
||||
public enum BlockType
|
||||
{
|
||||
/// <summary>
|
||||
/// Mono ATRAC9 block
|
||||
/// </summary>
|
||||
Mono = 0,
|
||||
/// <summary>
|
||||
/// Stereo ATRAC9 block
|
||||
/// </summary>
|
||||
Stereo = 1,
|
||||
/// <summary>
|
||||
/// Low-frequency effects ATRAC9 block
|
||||
/// </summary>
|
||||
LFE = 2
|
||||
}
|
||||
}
|
||||
@@ -1,49 +0,0 @@
|
||||
#nullable disable
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal class Channel
|
||||
{
|
||||
public Atrac9Config Config { get; }
|
||||
public int ChannelIndex { get; }
|
||||
public bool IsPrimary => Block.PrimaryChannelIndex == ChannelIndex;
|
||||
public Block Block { get; }
|
||||
public Mdct Mdct { get; }
|
||||
|
||||
public double[] Pcm { get; } = new double[256];
|
||||
public double[] Spectra { get; } = new double[256];
|
||||
|
||||
public int CodedQuantUnits { get; set; }
|
||||
public int ScaleFactorCodingMode { get; set; }
|
||||
public int[] ScaleFactors { get; } = new int[31];
|
||||
public int[] ScaleFactorsPrev { get; } = new int[31];
|
||||
|
||||
public int[] Precisions { get; } = new int[30];
|
||||
public int[] PrecisionsFine { get; } = new int[30];
|
||||
public int[] PrecisionMask { get; } = new int[30];
|
||||
|
||||
public int[] SpectraValuesBuffer { get; } = new int[16];
|
||||
public int[] CodebookSet { get; } = new int[30];
|
||||
|
||||
public int[] QuantizedSpectra { get; } = new int[256];
|
||||
public int[] QuantizedSpectraFine { get; } = new int[256];
|
||||
|
||||
public int BexMode { get; set; }
|
||||
public int BexValueCount { get; set; }
|
||||
public int[] BexValues { get; } = new int[4];
|
||||
public double[] BexScales { get; } = new double[6];
|
||||
public Atrac9Rng Rng { get; set; }
|
||||
|
||||
public Channel(Block parentBlock, int channelIndex)
|
||||
{
|
||||
Block = parentBlock;
|
||||
ChannelIndex = channelIndex;
|
||||
Config = parentBlock.Config;
|
||||
Mdct = new Mdct(Config.FrameSamplesPower, Tables.ImdctWindow[Config.FrameSamplesPower - 6]);
|
||||
}
|
||||
|
||||
public void UpdateCodedUnits() =>
|
||||
CodedQuantUnits = IsPrimary ? Block.QuantizationUnitCount : Block.StereoQuantizationUnit;
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the channel mapping for an ATRAC9 stream
|
||||
/// </summary>
|
||||
public class ChannelConfig
|
||||
{
|
||||
internal ChannelConfig(params BlockType[] blockTypes)
|
||||
{
|
||||
BlockCount = blockTypes.Length;
|
||||
BlockTypes = blockTypes;
|
||||
foreach (BlockType type in blockTypes)
|
||||
{
|
||||
ChannelCount += Block.BlockTypeToChannelCount(type);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of blocks or substreams in the ATRAC9 stream
|
||||
/// </summary>
|
||||
public int BlockCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of each block or substream in the ATRAC9 stream
|
||||
/// </summary>
|
||||
public BlockType[] BlockTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of channels in the ATRAC9 stream
|
||||
/// </summary>
|
||||
public int ChannelCount { get; }
|
||||
}
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal class Frame
|
||||
{
|
||||
public Atrac9Config Config { get; }
|
||||
public int FrameIndex { get; set; }
|
||||
public Block[] Blocks { get; }
|
||||
|
||||
public Frame(Atrac9Config config)
|
||||
{
|
||||
Config = config;
|
||||
Blocks = new Block[config.ChannelConfig.BlockCount];
|
||||
|
||||
for (int i = 0; i < config.ChannelConfig.BlockCount; i++)
|
||||
{
|
||||
Blocks[i] = new Block(this, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,63 +0,0 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal class HuffmanCodebook
|
||||
{
|
||||
public HuffmanCodebook(short[] codes, byte[] bits, byte valueCountPower)
|
||||
{
|
||||
Codes = codes;
|
||||
Bits = bits;
|
||||
if (Codes == null || Bits == null) return;
|
||||
|
||||
ValueCount = 1 << valueCountPower;
|
||||
ValueCountPower = valueCountPower;
|
||||
ValueBits = Helpers.Log2(codes.Length) >> valueCountPower;
|
||||
ValueMax = 1 << ValueBits;
|
||||
|
||||
int max = 0;
|
||||
foreach (byte bitSize in bits)
|
||||
{
|
||||
max = Math.Max(max, bitSize);
|
||||
}
|
||||
|
||||
MaxBitSize = max;
|
||||
Lookup = CreateLookupTable();
|
||||
}
|
||||
|
||||
private byte[] CreateLookupTable()
|
||||
{
|
||||
if (Codes == null || Bits == null) return null;
|
||||
|
||||
int tableSize = 1 << MaxBitSize;
|
||||
var dest = new byte[tableSize];
|
||||
|
||||
for (int i = 0; i < Bits.Length; i++)
|
||||
{
|
||||
if (Bits[i] == 0) continue;
|
||||
int unusedBits = MaxBitSize - Bits[i];
|
||||
|
||||
int start = Codes[i] << unusedBits;
|
||||
int length = 1 << unusedBits;
|
||||
int end = start + length;
|
||||
|
||||
for (int j = start; j < end; j++)
|
||||
{
|
||||
dest[j] = (byte)i;
|
||||
}
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
public short[] Codes { get; }
|
||||
public byte[] Bits { get; }
|
||||
public byte[] Lookup { get; }
|
||||
public int ValueCount { get; }
|
||||
public int ValueCountPower { get; }
|
||||
public int ValueBits { get; }
|
||||
public int ValueMax { get; }
|
||||
public int MaxBitSize { get; }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,21 +0,0 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Alex Barney
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,58 +0,0 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class Quantization
|
||||
{
|
||||
public static void DequantizeSpectra(Block block)
|
||||
{
|
||||
foreach (Channel channel in block.Channels)
|
||||
{
|
||||
Array.Clear(channel.Spectra, 0, channel.Spectra.Length);
|
||||
|
||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
||||
{
|
||||
DequantizeQuantUnit(channel, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DequantizeQuantUnit(Channel channel, int band)
|
||||
{
|
||||
int subBandIndex = Tables.QuantUnitToCoeffIndex[band];
|
||||
int subBandCount = Tables.QuantUnitToCoeffCount[band];
|
||||
double stepSize = Tables.QuantizerStepSize[channel.Precisions[band]];
|
||||
double stepSizeFine = Tables.QuantizerFineStepSize[channel.PrecisionsFine[band]];
|
||||
|
||||
for (int sb = 0; sb < subBandCount; sb++)
|
||||
{
|
||||
double coarse = channel.QuantizedSpectra[subBandIndex + sb] * stepSize;
|
||||
double fine = channel.QuantizedSpectraFine[subBandIndex + sb] * stepSizeFine;
|
||||
channel.Spectra[subBandIndex + sb] = coarse + fine;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ScaleSpectrum(Block block)
|
||||
{
|
||||
foreach (Channel channel in block.Channels)
|
||||
{
|
||||
ScaleSpectrum(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ScaleSpectrum(Channel channel)
|
||||
{
|
||||
int quantUnitCount = channel.Block.QuantizationUnitCount;
|
||||
double[] spectra = channel.Spectra;
|
||||
|
||||
for (int i = 0; i < quantUnitCount; i++)
|
||||
{
|
||||
for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++)
|
||||
{
|
||||
spectra[sb] *= Tables.SpectrumScale[channel.ScaleFactors[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,172 +0,0 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.IO;
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class ScaleFactors
|
||||
{
|
||||
public static void Read(BitReader reader, Channel channel)
|
||||
{
|
||||
Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length);
|
||||
|
||||
channel.ScaleFactorCodingMode = reader.ReadInt(2);
|
||||
if (channel.ChannelIndex == 0)
|
||||
{
|
||||
switch (channel.ScaleFactorCodingMode)
|
||||
{
|
||||
case 0:
|
||||
ReadVlcDeltaOffset(reader, channel);
|
||||
break;
|
||||
case 1:
|
||||
ReadClcOffset(reader, channel);
|
||||
break;
|
||||
case 2:
|
||||
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
|
||||
ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
|
||||
break;
|
||||
case 3:
|
||||
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
|
||||
ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (channel.ScaleFactorCodingMode)
|
||||
{
|
||||
case 0:
|
||||
ReadVlcDeltaOffset(reader, channel);
|
||||
break;
|
||||
case 1:
|
||||
ReadVlcDistanceToBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit);
|
||||
break;
|
||||
case 2:
|
||||
ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit);
|
||||
break;
|
||||
case 3:
|
||||
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
|
||||
ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
if (channel.ScaleFactors[i] < 0 || channel.ScaleFactors[i] > 31)
|
||||
{
|
||||
throw new InvalidDataException("Scale factor values are out of range.");
|
||||
}
|
||||
}
|
||||
|
||||
Array.Copy(channel.ScaleFactors, channel.ScaleFactorsPrev, channel.ScaleFactors.Length);
|
||||
}
|
||||
|
||||
private static void ReadClcOffset(BitReader reader, Channel channel)
|
||||
{
|
||||
const int maxBits = 5;
|
||||
int[] sf = channel.ScaleFactors;
|
||||
int bitLength = reader.ReadInt(2) + 2;
|
||||
int baseValue = bitLength < maxBits ? reader.ReadInt(maxBits) : 0;
|
||||
|
||||
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
sf[i] = reader.ReadInt(bitLength) + baseValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadVlcDeltaOffset(BitReader reader, Channel channel)
|
||||
{
|
||||
int weightIndex = reader.ReadInt(3);
|
||||
byte[] weights = ScaleFactorWeights[weightIndex];
|
||||
|
||||
int[] sf = channel.ScaleFactors;
|
||||
int baseValue = reader.ReadInt(5);
|
||||
int bitLength = reader.ReadInt(2) + 3;
|
||||
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength];
|
||||
|
||||
sf[0] = reader.ReadInt(bitLength);
|
||||
|
||||
for (int i = 1; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
int delta = Unpack.ReadHuffmanValue(codebook, reader);
|
||||
sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
sf[i] += baseValue - weights[i];
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadVlcDistanceToBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength)
|
||||
{
|
||||
int[] sf = channel.ScaleFactors;
|
||||
int bitLength = reader.ReadInt(2) + 2;
|
||||
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsSigned[bitLength];
|
||||
int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength);
|
||||
|
||||
for (int i = 0; i < unitCount; i++)
|
||||
{
|
||||
int distance = Unpack.ReadHuffmanValue(codebook, reader, true);
|
||||
sf[i] = (baseline[i] + distance) & 31;
|
||||
}
|
||||
|
||||
for (int i = unitCount; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
sf[i] = reader.ReadInt(5);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadVlcDeltaOffsetWithBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength)
|
||||
{
|
||||
int[] sf = channel.ScaleFactors;
|
||||
int baseValue = reader.ReadOffsetBinary(5, BitReader.OffsetBias.Negative);
|
||||
int bitLength = reader.ReadInt(2) + 1;
|
||||
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength];
|
||||
int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength);
|
||||
|
||||
sf[0] = reader.ReadInt(bitLength);
|
||||
|
||||
for (int i = 1; i < unitCount; i++)
|
||||
{
|
||||
int delta = Unpack.ReadHuffmanValue(codebook, reader);
|
||||
sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < unitCount; i++)
|
||||
{
|
||||
sf[i] += baseValue + baseline[i];
|
||||
}
|
||||
|
||||
for (int i = unitCount; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
sf[i] = reader.ReadInt(5);
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly byte[][] ScaleFactorWeights =
|
||||
{
|
||||
new byte[] {
|
||||
0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 2, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 10, 12, 12, 12
|
||||
}, new byte[] {
|
||||
3, 2, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 2, 3, 3, 4, 5, 7, 10, 10, 10
|
||||
}, new byte[] {
|
||||
0, 2, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 8, 9, 12, 12, 12
|
||||
}, new byte[] {
|
||||
0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 8, 8, 10, 11, 11, 12, 13, 13, 13, 13
|
||||
}, new byte[] {
|
||||
0, 2, 2, 3, 3, 4, 4, 5, 4, 5, 5, 5, 5, 6, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 11, 12, 12, 13, 13, 14, 14, 14
|
||||
}, new byte[] {
|
||||
1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 6, 7, 7, 9, 11, 11, 11
|
||||
}, new byte[] {
|
||||
0, 5, 8, 10, 11, 11, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 12,
|
||||
12, 13, 15, 15, 15
|
||||
}, new byte[] {
|
||||
0, 2, 3, 4, 5, 6, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13,
|
||||
15, 15, 15
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class Stereo
|
||||
{
|
||||
public static void ApplyIntensityStereo(Block block)
|
||||
{
|
||||
if (block.BlockType != BlockType.Stereo) return;
|
||||
|
||||
int totalUnits = block.QuantizationUnitCount;
|
||||
int stereoUnits = block.StereoQuantizationUnit;
|
||||
if (stereoUnits >= totalUnits) return;
|
||||
|
||||
Channel source = block.PrimaryChannel;
|
||||
Channel dest = block.SecondaryChannel;
|
||||
|
||||
for (int i = stereoUnits; i < totalUnits; i++)
|
||||
{
|
||||
int sign = block.JointStereoSigns[i];
|
||||
for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++)
|
||||
{
|
||||
if (sign > 0)
|
||||
{
|
||||
dest.Spectra[sb] = -source.Spectra[sb];
|
||||
}
|
||||
else
|
||||
{
|
||||
dest.Spectra[sb] = source.Spectra[sb];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,116 +0,0 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using static LibAtrac9.HuffmanCodebooks;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class Tables
|
||||
{
|
||||
public static int MaxHuffPrecision(bool highSampleRate) => highSampleRate ? 1 : 7;
|
||||
public static int MinBandCount(bool highSampleRate) => highSampleRate ? 1 : 3;
|
||||
public static int MaxExtensionBand(bool highSampleRate) => highSampleRate ? 16 : 18;
|
||||
|
||||
public static readonly int[] SampleRates =
|
||||
{
|
||||
11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000,
|
||||
44100, 48000, 64000, 88200, 96000, 128000, 176400, 192000
|
||||
};
|
||||
|
||||
public static readonly byte[] SamplingRateIndexToFrameSamplesPower = { 6, 6, 7, 7, 7, 8, 8, 8, 6, 6, 7, 7, 7, 8, 8, 8 };
|
||||
|
||||
// From sampling rate index
|
||||
public static readonly byte[] MaxBandCount = { 8, 8, 12, 12, 12, 18, 18, 18, 8, 8, 12, 12, 12, 16, 16, 16 };
|
||||
public static readonly byte[] BandToQuantUnitCount = { 0, 4, 8, 10, 12, 13, 14, 15, 16, 18, 20, 21, 22, 23, 24, 25, 26, 28, 30 };
|
||||
|
||||
public static readonly byte[] QuantUnitToCoeffCount =
|
||||
{
|
||||
02, 02, 02, 02, 02, 02, 02, 02, 04, 04, 04, 04, 08, 08, 08,
|
||||
08, 08, 08, 08, 08, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16
|
||||
};
|
||||
|
||||
public static readonly short[] QuantUnitToCoeffIndex =
|
||||
{
|
||||
0, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56,
|
||||
64, 72, 80, 88, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256
|
||||
};
|
||||
|
||||
public static readonly byte[] QuantUnitToCodebookIndex =
|
||||
{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3
|
||||
};
|
||||
|
||||
public static readonly ChannelConfig[] ChannelConfig =
|
||||
{
|
||||
new ChannelConfig(BlockType.Mono),
|
||||
new ChannelConfig(BlockType.Mono, BlockType.Mono),
|
||||
new ChannelConfig(BlockType.Stereo),
|
||||
new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo),
|
||||
new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo, BlockType.Stereo),
|
||||
new ChannelConfig(BlockType.Stereo, BlockType.Stereo)
|
||||
};
|
||||
|
||||
public static readonly HuffmanCodebook[] HuffmanScaleFactorsUnsigned =
|
||||
GenerateHuffmanCodebooks(HuffmanScaleFactorsACodes, HuffmanScaleFactorsABits, HuffmanScaleFactorsGroupSizes);
|
||||
|
||||
public static readonly HuffmanCodebook[] HuffmanScaleFactorsSigned =
|
||||
GenerateHuffmanCodebooks(HuffmanScaleFactorsBCodes, HuffmanScaleFactorsBBits, HuffmanScaleFactorsGroupSizes);
|
||||
|
||||
public static readonly HuffmanCodebook[][][] HuffmanSpectrum =
|
||||
{
|
||||
GenerateHuffmanCodebooks(HuffmanSpectrumACodes, HuffmanSpectrumABits, HuffmanSpectrumAGroupSizes),
|
||||
GenerateHuffmanCodebooks(HuffmanSpectrumBCodes, HuffmanSpectrumBBits, HuffmanSpectrumBGroupSizes)
|
||||
};
|
||||
|
||||
public static readonly double[][] ImdctWindow = { GenerateImdctWindow(6), GenerateImdctWindow(7), GenerateImdctWindow(8) };
|
||||
|
||||
public static readonly double[] SpectrumScale = Generate(32, SpectrumScaleFunction);
|
||||
public static readonly double[] QuantizerStepSize = Generate(16, QuantizerStepSizeFunction);
|
||||
public static readonly double[] QuantizerFineStepSize = Generate(16, QuantizerFineStepSizeFunction);
|
||||
|
||||
public static readonly byte[][] GradientCurves = BitAllocation.GenerateGradientCurves();
|
||||
|
||||
private static double QuantizerStepSizeFunction(int x) => 2.0 / ((1 << (x + 1)) - 1);
|
||||
private static double QuantizerFineStepSizeFunction(int x) => QuantizerStepSizeFunction(x) / ushort.MaxValue;
|
||||
private static double SpectrumScaleFunction(int x) => Math.Pow(2, x - 15);
|
||||
|
||||
private static double[] GenerateImdctWindow(int frameSizePower)
|
||||
{
|
||||
int frameSize = 1 << frameSizePower;
|
||||
var output = new double[frameSize];
|
||||
|
||||
double[] a1 = GenerateMdctWindow(frameSizePower);
|
||||
|
||||
for (int i = 0; i < frameSize; i++)
|
||||
{
|
||||
output[i] = a1[i] / (a1[frameSize - 1 - i] * a1[frameSize - 1 - i] + a1[i] * a1[i]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private static double[] GenerateMdctWindow(int frameSizePower)
|
||||
{
|
||||
int frameSize = 1 << frameSizePower;
|
||||
var output = new double[frameSize];
|
||||
|
||||
for (int i = 0; i < frameSize; i++)
|
||||
{
|
||||
output[i] = (Math.Sin(((i + 0.5) / frameSize - 0.5) * Math.PI) + 1.0) * 0.5;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static T[] Generate<T>(int count, Func<int, T> elementGenerator)
|
||||
{
|
||||
var table = new T[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
table[i] = elementGenerator(i);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -1,426 +0,0 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.IO;
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class Unpack
|
||||
{
|
||||
public static void UnpackFrame(BitReader reader, Frame frame)
|
||||
{
|
||||
foreach (Block block in frame.Blocks)
|
||||
{
|
||||
UnpackBlock(reader, block);
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnpackBlock(BitReader reader, Block block)
|
||||
{
|
||||
ReadBlockHeader(reader, block);
|
||||
|
||||
if (block.BlockType == BlockType.LFE)
|
||||
{
|
||||
UnpackLfeBlock(reader, block);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnpackStandardBlock(reader, block);
|
||||
}
|
||||
|
||||
reader.AlignPosition(8);
|
||||
}
|
||||
|
||||
private static void ReadBlockHeader(BitReader reader, Block block)
|
||||
{
|
||||
bool firstInSuperframe = block.Frame.FrameIndex == 0;
|
||||
block.FirstInSuperframe = !reader.ReadBool();
|
||||
block.ReuseBandParams = reader.ReadBool();
|
||||
|
||||
if (block.FirstInSuperframe != firstInSuperframe)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
|
||||
if (firstInSuperframe && block.ReuseBandParams && block.BlockType != BlockType.LFE)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnpackStandardBlock(BitReader reader, Block block)
|
||||
{
|
||||
Channel[] channels = block.Channels;
|
||||
|
||||
if (!block.ReuseBandParams)
|
||||
{
|
||||
ReadBandParams(reader, block);
|
||||
}
|
||||
|
||||
ReadGradientParams(reader, block);
|
||||
BitAllocation.CreateGradient(block);
|
||||
ReadStereoParams(reader, block);
|
||||
ReadExtensionParams(reader, block);
|
||||
|
||||
foreach (Channel channel in channels)
|
||||
{
|
||||
channel.UpdateCodedUnits();
|
||||
|
||||
ScaleFactors.Read(reader, channel);
|
||||
BitAllocation.CalculateMask(channel);
|
||||
BitAllocation.CalculatePrecisions(channel);
|
||||
CalculateSpectrumCodebookIndex(channel);
|
||||
|
||||
ReadSpectra(reader, channel);
|
||||
ReadSpectraFine(reader, channel);
|
||||
}
|
||||
|
||||
block.QuantizationUnitsPrev = block.BandExtensionEnabled ? block.ExtensionUnit : block.QuantizationUnitCount;
|
||||
}
|
||||
|
||||
private static void ReadBandParams(BitReader reader, Block block)
|
||||
{
|
||||
int minBandCount = Tables.MinBandCount(block.Config.HighSampleRate);
|
||||
int maxExtensionBand = Tables.MaxExtensionBand(block.Config.HighSampleRate);
|
||||
block.BandCount = reader.ReadInt(4);
|
||||
block.BandCount += minBandCount;
|
||||
block.QuantizationUnitCount = Tables.BandToQuantUnitCount[block.BandCount];
|
||||
if (block.BandCount < minBandCount || block.BandCount >
|
||||
Tables.MaxBandCount[block.Config.SampleRateIndex])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.BlockType == BlockType.Stereo)
|
||||
{
|
||||
block.StereoBand = reader.ReadInt(4);
|
||||
block.StereoBand += minBandCount;
|
||||
block.StereoQuantizationUnit = Tables.BandToQuantUnitCount[block.StereoBand];
|
||||
}
|
||||
else
|
||||
{
|
||||
block.StereoBand = block.BandCount;
|
||||
}
|
||||
|
||||
block.BandExtensionEnabled = reader.ReadBool();
|
||||
if (block.BandExtensionEnabled)
|
||||
{
|
||||
block.ExtensionBand = reader.ReadInt(4);
|
||||
block.ExtensionBand += minBandCount;
|
||||
|
||||
if (block.ExtensionBand < block.BandCount || block.ExtensionBand > maxExtensionBand)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
|
||||
block.ExtensionUnit = Tables.BandToQuantUnitCount[block.ExtensionBand];
|
||||
}
|
||||
else
|
||||
{
|
||||
block.ExtensionBand = block.BandCount;
|
||||
block.ExtensionUnit = block.QuantizationUnitCount;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadGradientParams(BitReader reader, Block block)
|
||||
{
|
||||
block.GradientMode = reader.ReadInt(2);
|
||||
if (block.GradientMode > 0)
|
||||
{
|
||||
block.GradientEndUnit = 31;
|
||||
block.GradientEndValue = 31;
|
||||
block.GradientStartUnit = reader.ReadInt(5);
|
||||
block.GradientStartValue = reader.ReadInt(5);
|
||||
}
|
||||
else
|
||||
{
|
||||
block.GradientStartUnit = reader.ReadInt(6);
|
||||
block.GradientEndUnit = reader.ReadInt(6) + 1;
|
||||
block.GradientStartValue = reader.ReadInt(5);
|
||||
block.GradientEndValue = reader.ReadInt(5);
|
||||
}
|
||||
block.GradientBoundary = reader.ReadInt(4);
|
||||
|
||||
if (block.GradientBoundary > block.QuantizationUnitCount)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
if (block.GradientStartUnit < 1 || block.GradientStartUnit >= 48)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
if (block.GradientEndUnit < 1 || block.GradientEndUnit >= 48)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
if (block.GradientStartUnit > block.GradientEndUnit)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
if (block.GradientStartValue < 0 || block.GradientStartValue >= 32)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
if (block.GradientEndValue < 0 || block.GradientEndValue >= 32)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadStereoParams(BitReader reader, Block block)
|
||||
{
|
||||
if (block.BlockType != BlockType.Stereo) return;
|
||||
|
||||
block.PrimaryChannelIndex = reader.ReadInt(1);
|
||||
block.HasJointStereoSigns = reader.ReadBool();
|
||||
if (block.HasJointStereoSigns)
|
||||
{
|
||||
for (int i = block.StereoQuantizationUnit; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
block.JointStereoSigns[i] = reader.ReadInt(1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Clear(block.JointStereoSigns, 0, block.JointStereoSigns.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadExtensionParams(BitReader reader, Block block)
|
||||
{
|
||||
// ReSharper disable once RedundantAssignment
|
||||
int bexBand = 0;
|
||||
if (block.BandExtensionEnabled)
|
||||
{
|
||||
BandExtension.GetBexBandInfo(out bexBand, out _, out _, block.QuantizationUnitCount);
|
||||
if (block.BlockType == BlockType.Stereo)
|
||||
{
|
||||
ReadHeader(block.Channels[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Position += 1;
|
||||
}
|
||||
}
|
||||
block.HasExtensionData = reader.ReadBool();
|
||||
|
||||
if (!block.HasExtensionData) return;
|
||||
if (!block.BandExtensionEnabled)
|
||||
{
|
||||
block.BexMode = reader.ReadInt(2);
|
||||
block.BexDataLength = reader.ReadInt(5);
|
||||
reader.Position += block.BexDataLength;
|
||||
return;
|
||||
}
|
||||
|
||||
ReadHeader(block.Channels[0]);
|
||||
|
||||
block.BexDataLength = reader.ReadInt(5);
|
||||
if (block.BexDataLength <= 0) return;
|
||||
int bexDataEnd = reader.Position + block.BexDataLength;
|
||||
|
||||
ReadData(block.Channels[0]);
|
||||
|
||||
if (block.BlockType == BlockType.Stereo)
|
||||
{
|
||||
ReadData(block.Channels[1]);
|
||||
}
|
||||
|
||||
// Make sure we didn't read too many bits
|
||||
if (reader.Position > bexDataEnd)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
|
||||
void ReadHeader(Channel channel)
|
||||
{
|
||||
int bexMode = reader.ReadInt(2);
|
||||
channel.BexMode = bexBand > 2 ? bexMode : 4;
|
||||
channel.BexValueCount = BandExtension.BexEncodedValueCounts[channel.BexMode][bexBand];
|
||||
}
|
||||
|
||||
void ReadData(Channel channel)
|
||||
{
|
||||
for (int i = 0; i < channel.BexValueCount; i++)
|
||||
{
|
||||
int dataLength = BandExtension.BexDataLengths[channel.BexMode][bexBand][i];
|
||||
channel.BexValues[i] = reader.ReadInt(dataLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CalculateSpectrumCodebookIndex(Channel channel)
|
||||
{
|
||||
Array.Clear(channel.CodebookSet, 0, channel.CodebookSet.Length);
|
||||
int quantUnits = channel.CodedQuantUnits;
|
||||
int[] sf = channel.ScaleFactors;
|
||||
|
||||
if (quantUnits <= 1) return;
|
||||
if (channel.Config.HighSampleRate) return;
|
||||
|
||||
// Temporarily setting this value allows for simpler code by
|
||||
// making the last value a non-special case.
|
||||
int originalScaleTmp = sf[quantUnits];
|
||||
sf[quantUnits] = sf[quantUnits - 1];
|
||||
|
||||
int avg = 0;
|
||||
if (quantUnits > 12)
|
||||
{
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
avg += sf[i];
|
||||
}
|
||||
avg = (avg + 6) / 12;
|
||||
}
|
||||
|
||||
for (int i = 8; i < quantUnits; i++)
|
||||
{
|
||||
int prevSf = sf[i - 1];
|
||||
int nextSf = sf[i + 1];
|
||||
int minSf = Math.Min(prevSf, nextSf);
|
||||
if (sf[i] - minSf >= 3 || sf[i] - prevSf + sf[i] - nextSf >= 3)
|
||||
{
|
||||
channel.CodebookSet[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 12; i < quantUnits; i++)
|
||||
{
|
||||
if (channel.CodebookSet[i] == 0)
|
||||
{
|
||||
int minSf = Math.Min(sf[i - 1], sf[i + 1]);
|
||||
if (sf[i] - minSf >= 2 && sf[i] >= avg - (Tables.QuantUnitToCoeffCount[i] == 16 ? 1 : 0))
|
||||
{
|
||||
channel.CodebookSet[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sf[quantUnits] = originalScaleTmp;
|
||||
}
|
||||
|
||||
private static void ReadSpectra(BitReader reader, Channel channel)
|
||||
{
|
||||
int[] values = channel.SpectraValuesBuffer;
|
||||
Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length);
|
||||
int maxHuffPrecision = Tables.MaxHuffPrecision(channel.Config.HighSampleRate);
|
||||
|
||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
||||
{
|
||||
int subbandCount = Tables.QuantUnitToCoeffCount[i];
|
||||
int precision = channel.Precisions[i] + 1;
|
||||
if (precision <= maxHuffPrecision)
|
||||
{
|
||||
HuffmanCodebook huff = Tables.HuffmanSpectrum[channel.CodebookSet[i]][precision][Tables.QuantUnitToCodebookIndex[i]];
|
||||
int groupCount = subbandCount >> huff.ValueCountPower;
|
||||
for (int j = 0; j < groupCount; j++)
|
||||
{
|
||||
values[j] = ReadHuffmanValue(huff, reader);
|
||||
}
|
||||
|
||||
DecodeHuffmanValues(channel.QuantizedSpectra, Tables.QuantUnitToCoeffIndex[i], subbandCount, huff, values);
|
||||
}
|
||||
else
|
||||
{
|
||||
int subbandIndex = Tables.QuantUnitToCoeffIndex[i];
|
||||
for (int j = subbandIndex; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++)
|
||||
{
|
||||
channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadSpectraFine(BitReader reader, Channel channel)
|
||||
{
|
||||
Array.Clear(channel.QuantizedSpectraFine, 0, channel.QuantizedSpectraFine.Length);
|
||||
|
||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
||||
{
|
||||
if (channel.PrecisionsFine[i] > 0)
|
||||
{
|
||||
int overflowBits = channel.PrecisionsFine[i] + 1;
|
||||
int startSubband = Tables.QuantUnitToCoeffIndex[i];
|
||||
int endSubband = Tables.QuantUnitToCoeffIndex[i + 1];
|
||||
|
||||
for (int j = startSubband; j < endSubband; j++)
|
||||
{
|
||||
channel.QuantizedSpectraFine[j] = reader.ReadSignedInt(overflowBits);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DecodeHuffmanValues(int[] spectrum, int index, int bandCount, HuffmanCodebook huff, int[] values)
|
||||
{
|
||||
int valueCount = bandCount >> huff.ValueCountPower;
|
||||
int mask = (1 << huff.ValueBits) - 1;
|
||||
|
||||
for (int i = 0; i < valueCount; i++)
|
||||
{
|
||||
int value = values[i];
|
||||
for (int j = 0; j < huff.ValueCount; j++)
|
||||
{
|
||||
spectrum[index++] = Bit.SignExtend32(value & mask, huff.ValueBits);
|
||||
value >>= huff.ValueBits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int ReadHuffmanValue(HuffmanCodebook huff, BitReader reader, bool signed = false)
|
||||
{
|
||||
int code = reader.PeekInt(huff.MaxBitSize);
|
||||
byte value = huff.Lookup[code];
|
||||
int bits = huff.Bits[value];
|
||||
reader.Position += bits;
|
||||
return signed ? Bit.SignExtend32(value, huff.ValueBits) : value;
|
||||
}
|
||||
|
||||
private static void UnpackLfeBlock(BitReader reader, Block block)
|
||||
{
|
||||
Channel channel = block.Channels[0];
|
||||
block.QuantizationUnitCount = 2;
|
||||
|
||||
DecodeLfeScaleFactors(reader, channel);
|
||||
CalculateLfePrecision(channel);
|
||||
channel.CodedQuantUnits = block.QuantizationUnitCount;
|
||||
ReadLfeSpectra(reader, channel);
|
||||
}
|
||||
|
||||
private static void DecodeLfeScaleFactors(BitReader reader, Channel channel)
|
||||
{
|
||||
Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length);
|
||||
for (int i = 0; i < channel.Block.QuantizationUnitCount; i++)
|
||||
{
|
||||
channel.ScaleFactors[i] = reader.ReadInt(5);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CalculateLfePrecision(Channel channel)
|
||||
{
|
||||
Block block = channel.Block;
|
||||
int precision = block.ReuseBandParams ? 8 : 4;
|
||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
channel.Precisions[i] = precision;
|
||||
channel.PrecisionsFine[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadLfeSpectra(BitReader reader, Channel channel)
|
||||
{
|
||||
Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length);
|
||||
|
||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
||||
{
|
||||
if (channel.Precisions[i] <= 0) continue;
|
||||
|
||||
int precision = channel.Precisions[i] + 1;
|
||||
for (int j = Tables.QuantUnitToCoeffIndex[i]; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++)
|
||||
{
|
||||
channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,23 +0,0 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9.Utilities
|
||||
{
|
||||
internal static class Bit
|
||||
{
|
||||
private static uint BitReverse32(uint value)
|
||||
{
|
||||
value = ((value & 0xaaaaaaaa) >> 1) | ((value & 0x55555555) << 1);
|
||||
value = ((value & 0xcccccccc) >> 2) | ((value & 0x33333333) << 2);
|
||||
value = ((value & 0xf0f0f0f0) >> 4) | ((value & 0x0f0f0f0f) << 4);
|
||||
value = ((value & 0xff00ff00) >> 8) | ((value & 0x00ff00ff) << 8);
|
||||
return (value >> 16) | (value << 16);
|
||||
}
|
||||
private static uint BitReverse32(uint value, int bitCount) => BitReverse32(value) >> (32 - bitCount);
|
||||
public static int BitReverse32(int value, int bitCount) => (int) BitReverse32((uint) value, bitCount);
|
||||
|
||||
public static int SignExtend32(int value, int bits)
|
||||
{
|
||||
int shift = 8 * sizeof(int) - bits;
|
||||
return (value << shift) >> shift;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,133 +0,0 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LibAtrac9.Utilities
|
||||
{
|
||||
internal class BitReader
|
||||
{
|
||||
private byte[] Buffer { get; set; }
|
||||
private int LengthBits { get; set; }
|
||||
public int Position { get; set; }
|
||||
private int Remaining => LengthBits - Position;
|
||||
|
||||
public BitReader(byte[] buffer) => SetBuffer(buffer);
|
||||
|
||||
public void SetBuffer(byte[] buffer)
|
||||
{
|
||||
Buffer = buffer;
|
||||
LengthBits = Buffer?.Length * 8 ?? 0;
|
||||
Position = 0;
|
||||
}
|
||||
|
||||
public int ReadInt(int bitCount)
|
||||
{
|
||||
int value = PeekInt(bitCount);
|
||||
Position += bitCount;
|
||||
return value;
|
||||
}
|
||||
|
||||
public int ReadSignedInt(int bitCount)
|
||||
{
|
||||
int value = PeekInt(bitCount);
|
||||
Position += bitCount;
|
||||
return Bit.SignExtend32(value, bitCount);
|
||||
}
|
||||
|
||||
public bool ReadBool() => ReadInt(1) == 1;
|
||||
|
||||
public int ReadOffsetBinary(int bitCount, OffsetBias bias)
|
||||
{
|
||||
int offset = (1 << (bitCount - 1)) - (int)bias;
|
||||
int value = PeekInt(bitCount) - offset;
|
||||
Position += bitCount;
|
||||
return value;
|
||||
}
|
||||
|
||||
public void AlignPosition(int multiple)
|
||||
{
|
||||
Position = Helpers.GetNextMultiple(Position, multiple);
|
||||
}
|
||||
|
||||
public int PeekInt(int bitCount)
|
||||
{
|
||||
Debug.Assert(bitCount >= 0 && bitCount <= 32);
|
||||
|
||||
if (bitCount > Remaining)
|
||||
{
|
||||
if (Position >= LengthBits) return 0;
|
||||
|
||||
int extraBits = bitCount - Remaining;
|
||||
return PeekIntFallback(Remaining) << extraBits;
|
||||
}
|
||||
|
||||
int byteIndex = Position / 8;
|
||||
int bitIndex = Position % 8;
|
||||
|
||||
if (bitCount <= 9 && Remaining >= 16)
|
||||
{
|
||||
int value = Buffer[byteIndex] << 8 | Buffer[byteIndex + 1];
|
||||
value &= 0xFFFF >> bitIndex;
|
||||
value >>= 16 - bitCount - bitIndex;
|
||||
return value;
|
||||
}
|
||||
|
||||
if (bitCount <= 17 && Remaining >= 24)
|
||||
{
|
||||
int value = Buffer[byteIndex] << 16 | Buffer[byteIndex + 1] << 8 | Buffer[byteIndex + 2];
|
||||
value &= 0xFFFFFF >> bitIndex;
|
||||
value >>= 24 - bitCount - bitIndex;
|
||||
return value;
|
||||
}
|
||||
|
||||
if (bitCount <= 25 && Remaining >= 32)
|
||||
{
|
||||
int value = Buffer[byteIndex] << 24 | Buffer[byteIndex + 1] << 16 | Buffer[byteIndex + 2] << 8 | Buffer[byteIndex + 3];
|
||||
value &= (int)(0xFFFFFFFF >> bitIndex);
|
||||
value >>= 32 - bitCount - bitIndex;
|
||||
return value;
|
||||
}
|
||||
return PeekIntFallback(bitCount);
|
||||
}
|
||||
|
||||
private int PeekIntFallback(int bitCount)
|
||||
{
|
||||
int value = 0;
|
||||
int byteIndex = Position / 8;
|
||||
int bitIndex = Position % 8;
|
||||
|
||||
while (bitCount > 0)
|
||||
{
|
||||
if (bitIndex >= 8)
|
||||
{
|
||||
bitIndex = 0;
|
||||
byteIndex++;
|
||||
}
|
||||
|
||||
int bitsToRead = Math.Min(bitCount, 8 - bitIndex);
|
||||
int mask = 0xFF >> bitIndex;
|
||||
int currentByte = (mask & Buffer[byteIndex]) >> (8 - bitIndex - bitsToRead);
|
||||
|
||||
value = (value << bitsToRead) | currentByte;
|
||||
bitIndex += bitsToRead;
|
||||
bitCount -= bitsToRead;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the bias of an offset binary value. A positive bias can represent one more
|
||||
/// positive value than negative value, and a negative bias can represent one more
|
||||
/// negative value than positive value.
|
||||
/// </summary>
|
||||
/// <remarks>Example:
|
||||
/// A 4-bit offset binary value with a positive bias can store
|
||||
/// the values 8 through -7 inclusive.
|
||||
/// A 4-bit offset binary value with a positive bias can store
|
||||
/// the values 7 through -8 inclusive.</remarks>
|
||||
public enum OffsetBias
|
||||
{
|
||||
Negative = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,51 +0,0 @@
|
||||
#nullable disable
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace LibAtrac9.Utilities
|
||||
{
|
||||
internal static class Helpers
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static short Clamp16(int value)
|
||||
{
|
||||
if (value > short.MaxValue)
|
||||
return short.MaxValue;
|
||||
if (value < short.MinValue)
|
||||
return short.MinValue;
|
||||
return (short)value;
|
||||
}
|
||||
|
||||
public static int GetNextMultiple(int value, int multiple)
|
||||
{
|
||||
if (multiple <= 0)
|
||||
return value;
|
||||
|
||||
if (value % multiple == 0)
|
||||
return value;
|
||||
|
||||
return value + multiple - value % multiple;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the floor of the base 2 logarithm of a specified number.
|
||||
/// </summary>
|
||||
/// <param name="value">The number whose logarithm is to be found.</param>
|
||||
/// <returns>The floor of the base 2 logarithm of <paramref name="value"/>.</returns>
|
||||
public static int Log2(int value)
|
||||
{
|
||||
value |= value >> 1;
|
||||
value |= value >> 2;
|
||||
value |= value >> 4;
|
||||
value |= value >> 8;
|
||||
value |= value >> 16;
|
||||
|
||||
return MultiplyDeBruijnBitPosition[(uint)(value * 0x07C4ACDDU) >> 27];
|
||||
}
|
||||
|
||||
private static readonly int[] MultiplyDeBruijnBitPosition =
|
||||
{
|
||||
0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30,
|
||||
8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,178 +0,0 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LibAtrac9.Utilities
|
||||
{
|
||||
internal class Mdct
|
||||
{
|
||||
private int MdctBits { get; }
|
||||
private int MdctSize { get; }
|
||||
private double Scale { get; }
|
||||
|
||||
private static readonly object TableLock = new object();
|
||||
private static int _tableBits = -1;
|
||||
private static readonly List<double[]> SinTables = new List<double[]>();
|
||||
private static readonly List<double[]> CosTables = new List<double[]>();
|
||||
private static readonly List<int[]> ShuffleTables = new List<int[]>();
|
||||
|
||||
private readonly double[] _imdctPrevious;
|
||||
private readonly double[] _imdctWindow;
|
||||
|
||||
private readonly double[] _scratchMdct;
|
||||
private readonly double[] _scratchDct;
|
||||
|
||||
public Mdct(int mdctBits, double[] window, double scale = 1)
|
||||
{
|
||||
SetTables(mdctBits);
|
||||
|
||||
MdctBits = mdctBits;
|
||||
MdctSize = 1 << mdctBits;
|
||||
Scale = scale;
|
||||
|
||||
if (window.Length < MdctSize)
|
||||
{
|
||||
throw new ArgumentException("Window must be as long as the MDCT size.", nameof(window));
|
||||
}
|
||||
|
||||
_imdctPrevious = new double[MdctSize];
|
||||
_scratchMdct = new double[MdctSize];
|
||||
_scratchDct = new double[MdctSize];
|
||||
_imdctWindow = window;
|
||||
}
|
||||
|
||||
private static void SetTables(int maxBits)
|
||||
{
|
||||
lock (TableLock)
|
||||
{
|
||||
if (maxBits > _tableBits)
|
||||
{
|
||||
for (int i = _tableBits + 1; i <= maxBits; i++)
|
||||
{
|
||||
GenerateTrigTables(i, out double[] sin, out double[] cos);
|
||||
SinTables.Add(sin);
|
||||
CosTables.Add(cos);
|
||||
ShuffleTables.Add(GenerateShuffleTable(i));
|
||||
}
|
||||
_tableBits = maxBits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RunImdct(double[] input, double[] output)
|
||||
{
|
||||
if (input.Length < MdctSize)
|
||||
{
|
||||
throw new ArgumentException("Input must be as long as the MDCT size.", nameof(input));
|
||||
}
|
||||
|
||||
if (output.Length < MdctSize)
|
||||
{
|
||||
throw new ArgumentException("Output must be as long as the MDCT size.", nameof(output));
|
||||
}
|
||||
|
||||
int size = MdctSize;
|
||||
int half = size / 2;
|
||||
double[] dctOut = _scratchMdct;
|
||||
|
||||
Dct4(input, dctOut);
|
||||
|
||||
for (int i = 0; i < half; i++)
|
||||
{
|
||||
output[i] = _imdctWindow[i] * dctOut[i + half] + _imdctPrevious[i];
|
||||
output[i + half] = _imdctWindow[i + half] * -dctOut[size - 1 - i] - _imdctPrevious[i + half];
|
||||
_imdctPrevious[i] = _imdctWindow[size - 1 - i] * -dctOut[half - i - 1];
|
||||
_imdctPrevious[i + half] = _imdctWindow[half - i - 1] * dctOut[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does a Type-4 DCT.
|
||||
/// </summary>
|
||||
/// <param name="input">The input array containing the time or frequency-domain samples</param>
|
||||
/// <param name="output">The output array that will contain the transformed time or frequency-domain samples</param>
|
||||
private void Dct4(double[] input, double[] output)
|
||||
{
|
||||
int[] shuffleTable = ShuffleTables[MdctBits];
|
||||
double[] sinTable = SinTables[MdctBits];
|
||||
double[] cosTable = CosTables[MdctBits];
|
||||
double[] dctTemp = _scratchDct;
|
||||
|
||||
int size = MdctSize;
|
||||
int lastIndex = size - 1;
|
||||
int halfSize = size / 2;
|
||||
|
||||
for (int i = 0; i < halfSize; i++)
|
||||
{
|
||||
int i2 = i * 2;
|
||||
double a = input[i2];
|
||||
double b = input[lastIndex - i2];
|
||||
double sin = sinTable[i];
|
||||
double cos = cosTable[i];
|
||||
dctTemp[i2] = a * cos + b * sin;
|
||||
dctTemp[i2 + 1] = a * sin - b * cos;
|
||||
}
|
||||
int stageCount = MdctBits - 1;
|
||||
|
||||
for (int stage = 0; stage < stageCount; stage++)
|
||||
{
|
||||
int blockCount = 1 << stage;
|
||||
int blockSizeBits = stageCount - stage;
|
||||
int blockHalfSizeBits = blockSizeBits - 1;
|
||||
int blockSize = 1 << blockSizeBits;
|
||||
int blockHalfSize = 1 << blockHalfSizeBits;
|
||||
sinTable = SinTables[blockHalfSizeBits];
|
||||
cosTable = CosTables[blockHalfSizeBits];
|
||||
|
||||
for (int block = 0; block < blockCount; block++)
|
||||
{
|
||||
for (int i = 0; i < blockHalfSize; i++)
|
||||
{
|
||||
int frontPos = (block * blockSize + i) * 2;
|
||||
int backPos = frontPos + blockSize;
|
||||
double a = dctTemp[frontPos] - dctTemp[backPos];
|
||||
double b = dctTemp[frontPos + 1] - dctTemp[backPos + 1];
|
||||
double sin = sinTable[i];
|
||||
double cos = cosTable[i];
|
||||
dctTemp[frontPos] += dctTemp[backPos];
|
||||
dctTemp[frontPos + 1] += dctTemp[backPos + 1];
|
||||
dctTemp[backPos] = a * cos + b * sin;
|
||||
dctTemp[backPos + 1] = a * sin - b * cos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < MdctSize; i++)
|
||||
{
|
||||
output[i] = dctTemp[shuffleTable[i]] * Scale;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void GenerateTrigTables(int sizeBits, out double[] sin, out double[] cos)
|
||||
{
|
||||
int size = 1 << sizeBits;
|
||||
sin = new double[size];
|
||||
cos = new double[size];
|
||||
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
double value = Math.PI * (4 * i + 1) / (4 * size);
|
||||
sin[i] = Math.Sin(value);
|
||||
cos[i] = Math.Cos(value);
|
||||
}
|
||||
}
|
||||
|
||||
internal static int[] GenerateShuffleTable(int sizeBits)
|
||||
{
|
||||
int size = 1 << sizeBits;
|
||||
var table = new int[size];
|
||||
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
table[i] = Bit.BitReverse32(i ^ (i / 2), sizeBits);
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,252 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal Discord Rich Presence client. Speaks Discord's local IPC
|
||||
/// protocol (framed JSON over the discord-ipc-N named pipe) directly, so no
|
||||
/// external dependency is needed. All failures are silent: no Discord, no
|
||||
/// presence, no impact on the launcher.
|
||||
/// </summary>
|
||||
public sealed class DiscordRichPresence : IDisposable
|
||||
{
|
||||
private const int OpHandshake = 0;
|
||||
private const int OpFrame = 1;
|
||||
private const int MaxFrameBytes = 64 * 1024;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly string _clientId;
|
||||
private readonly AutoResetEvent _signal = new(false);
|
||||
private readonly Thread _worker;
|
||||
|
||||
private NamedPipeClientStream? _pipe;
|
||||
private PresenceState? _desired;
|
||||
private PresenceState? _sent;
|
||||
private volatile bool _disposed;
|
||||
|
||||
private sealed record PresenceState(string Details, string? State, long? StartUnixSeconds);
|
||||
|
||||
public DiscordRichPresence(string clientId)
|
||||
{
|
||||
_clientId = clientId;
|
||||
_worker = new Thread(WorkerLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "DiscordRichPresence",
|
||||
};
|
||||
_worker.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests the given presence; the worker applies it asynchronously and
|
||||
/// retries (with reconnection) until it is delivered or replaced.
|
||||
/// </summary>
|
||||
public void SetPresence(string details, string? state = null, long? startUnixSeconds = null)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_desired = new PresenceState(details, state, startUnixSeconds);
|
||||
}
|
||||
|
||||
Trace($"queued details=\"{details}\" state=\"{state}\"");
|
||||
_signal.Set();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
_signal.Set();
|
||||
ClosePipe();
|
||||
}
|
||||
|
||||
private void WorkerLoop()
|
||||
{
|
||||
while (!_disposed)
|
||||
{
|
||||
// Woken immediately on presence changes; the periodic tick
|
||||
// retries after connection failures (e.g. Discord starts later).
|
||||
_signal.WaitOne(TimeSpan.FromSeconds(15));
|
||||
if (_disposed || _clientId.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PresenceState? desired;
|
||||
lock (_gate)
|
||||
{
|
||||
desired = _desired;
|
||||
}
|
||||
|
||||
if (desired is null || desired == _sent)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EnsureConnected();
|
||||
SendActivity(desired);
|
||||
_sent = desired;
|
||||
Trace($"sent details=\"{desired.Details}\" state=\"{desired.State}\"");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Connection died (or never existed); reconnect on a later tick.
|
||||
Trace($"send failed: {exception.Message}");
|
||||
ClosePipe();
|
||||
_sent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (_pipe is { IsConnected: true })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClosePipe();
|
||||
Exception? lastError = null;
|
||||
for (var index = 0; index < 10; index++)
|
||||
{
|
||||
var pipe = new NamedPipeClientStream(
|
||||
".",
|
||||
$"discord-ipc-{index}",
|
||||
PipeDirection.InOut,
|
||||
PipeOptions.Asynchronous);
|
||||
try
|
||||
{
|
||||
pipe.Connect(500);
|
||||
_pipe = pipe;
|
||||
WriteFrame(OpHandshake, JsonSerializer.Serialize(new
|
||||
{
|
||||
v = 1,
|
||||
client_id = _clientId,
|
||||
}));
|
||||
|
||||
// Discord replies with a READY (or error) frame; consume it
|
||||
// so the pipe buffer stays drained.
|
||||
_ = ReadFrame();
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
lastError = exception;
|
||||
pipe.Dispose();
|
||||
_pipe = null;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new IOException("Discord IPC pipe not found.");
|
||||
}
|
||||
|
||||
private void SendActivity(PresenceState presence)
|
||||
{
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["cmd"] = "SET_ACTIVITY",
|
||||
["nonce"] = Guid.NewGuid().ToString(),
|
||||
["args"] = new Dictionary<string, object?>
|
||||
{
|
||||
["pid"] = Environment.ProcessId,
|
||||
["activity"] = new Dictionary<string, object?>
|
||||
{
|
||||
["details"] = presence.Details,
|
||||
["state"] = presence.State,
|
||||
["timestamps"] = presence.StartUnixSeconds is { } start
|
||||
? new Dictionary<string, object?> { ["start"] = start }
|
||||
: null,
|
||||
["assets"] = new Dictionary<string, object?>
|
||||
{
|
||||
["large_image"] = "logo",
|
||||
["large_text"] = "SharpEmu",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
WriteFrame(OpFrame, JsonSerializer.Serialize(payload, JsonOptions));
|
||||
|
||||
// Consume Discord's acknowledgement to keep the pipe drained.
|
||||
_ = ReadFrame();
|
||||
}
|
||||
|
||||
private void WriteFrame(int opcode, string json)
|
||||
{
|
||||
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
|
||||
var payload = Encoding.UTF8.GetBytes(json);
|
||||
var frame = new byte[8 + payload.Length];
|
||||
BitConverter.TryWriteBytes(frame.AsSpan(0, 4), opcode);
|
||||
BitConverter.TryWriteBytes(frame.AsSpan(4, 4), payload.Length);
|
||||
payload.CopyTo(frame, 8);
|
||||
pipe.Write(frame, 0, frame.Length);
|
||||
pipe.Flush();
|
||||
}
|
||||
|
||||
private string ReadFrame()
|
||||
{
|
||||
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
|
||||
var header = ReadExactly(pipe, 8);
|
||||
var length = BitConverter.ToInt32(header, 4);
|
||||
if (length is < 0 or > MaxFrameBytes)
|
||||
{
|
||||
throw new IOException($"Discord IPC frame length {length} is out of range.");
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(ReadExactly(pipe, length));
|
||||
}
|
||||
|
||||
private static byte[] ReadExactly(NamedPipeClientStream pipe, int count)
|
||||
{
|
||||
var buffer = new byte[count];
|
||||
var read = 0;
|
||||
while (read < count)
|
||||
{
|
||||
var chunk = pipe.Read(buffer, read, count - read);
|
||||
if (chunk <= 0)
|
||||
{
|
||||
throw new IOException("Discord IPC pipe closed.");
|
||||
}
|
||||
|
||||
read += chunk;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCORD"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[DISCORD] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ClosePipe()
|
||||
{
|
||||
var pipe = _pipe;
|
||||
_pipe = null;
|
||||
try
|
||||
{
|
||||
pipe?.Dispose();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Best effort; the pipe may already be broken.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,657 +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;
|
||||
}
|
||||
|
||||
// Prefer terminating the job: it kills the whole tree, including
|
||||
// any children the emulator spawned, even when the main process
|
||||
// is wedged in a GPU driver call.
|
||||
if (_jobHandle != 0)
|
||||
{
|
||||
_ = TerminateJobObject(_jobHandle, 1);
|
||||
}
|
||||
|
||||
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 TerminateJobObject(nint job, uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(nint handle);
|
||||
}
|
||||
@@ -1,156 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public sealed class GameEntry : INotifyPropertyChanged
|
||||
{
|
||||
// Placeholder gradients for games without cover art, picked
|
||||
// deterministically from the game name so a game keeps its color.
|
||||
private static readonly (Color Start, Color End)[] PlaceholderPalette =
|
||||
{
|
||||
(Color.Parse("#5B4B8A"), Color.Parse("#2C2A4A")),
|
||||
(Color.Parse("#1F6E8C"), Color.Parse("#173B45")),
|
||||
(Color.Parse("#7A4069"), Color.Parse("#3B1C32")),
|
||||
(Color.Parse("#2D6A4F"), Color.Parse("#1B3A2B")),
|
||||
(Color.Parse("#8C5425"), Color.Parse("#4A2B12")),
|
||||
(Color.Parse("#4F6D9E"), Color.Parse("#263349")),
|
||||
(Color.Parse("#8A4B4B"), Color.Parse("#3F2222")),
|
||||
(Color.Parse("#3E7C7B"), Color.Parse("#1E3D3C")),
|
||||
};
|
||||
|
||||
private Bitmap? _cover;
|
||||
private IBrush? _placeholderBrush;
|
||||
private long _sizeBytes;
|
||||
|
||||
public GameEntry(
|
||||
string name, string? titleId, string path, long sizeBytes, string? coverPath, string? backgroundPath)
|
||||
{
|
||||
Name = name;
|
||||
TitleId = titleId;
|
||||
Path = path;
|
||||
_sizeBytes = sizeBytes;
|
||||
CoverPath = coverPath;
|
||||
BackgroundPath = backgroundPath;
|
||||
Initials = ComputeInitials(name);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string? TitleId { get; }
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Total size of the game. Initially the eboot's own size from the scan;
|
||||
/// replaced with the full install folder size once computed in the
|
||||
/// background.
|
||||
/// </summary>
|
||||
public long SizeBytes
|
||||
{
|
||||
get => _sizeBytes;
|
||||
set
|
||||
{
|
||||
if (_sizeBytes == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_sizeBytes = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Detail)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Path to the cover art image shipped with the game, if found.</summary>
|
||||
public string? CoverPath { get; }
|
||||
|
||||
/// <summary>Path to the key art (pic0/pic1) shipped with the game, if found.</summary>
|
||||
public string? BackgroundPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Decoded key art used as the window backdrop while this game is
|
||||
/// selected. Loaded on demand and cached; not exposed via binding.
|
||||
/// </summary>
|
||||
public Bitmap? Background { get; set; }
|
||||
|
||||
public string Initials { get; }
|
||||
|
||||
// Built lazily: brushes are AvaloniaObjects that must be created on the
|
||||
// UI thread, while GameEntry itself is constructed on the scan thread.
|
||||
public IBrush PlaceholderBrush => _placeholderBrush ??= BuildPlaceholderBrush(Name);
|
||||
|
||||
/// <summary>Decoded cover art; loaded asynchronously after the library scan.</summary>
|
||||
public Bitmap? Cover
|
||||
{
|
||||
get => _cover;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(_cover, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cover = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Cover)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasCover)));
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasCover => _cover is not null;
|
||||
|
||||
public string Detail => TitleId is not null
|
||||
? $"{TitleId} • {FormatSize(SizeBytes)}"
|
||||
: FormatSize(SizeBytes);
|
||||
|
||||
private static string ComputeInitials(string name)
|
||||
{
|
||||
var initials = name
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(word => char.IsLetterOrDigit(word[0]))
|
||||
.Select(word => char.ToUpperInvariant(word[0]))
|
||||
.Take(2)
|
||||
.ToArray();
|
||||
|
||||
return initials.Length > 0 ? new string(initials) : "?";
|
||||
}
|
||||
|
||||
private static IBrush BuildPlaceholderBrush(string name)
|
||||
{
|
||||
var hash = 0;
|
||||
foreach (var ch in name)
|
||||
{
|
||||
hash = unchecked(hash * 31 + ch);
|
||||
}
|
||||
|
||||
var (start, end) = PlaceholderPalette[(int)((uint)hash % PlaceholderPalette.Length)];
|
||||
return new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops =
|
||||
{
|
||||
new GradientStop(start, 0),
|
||||
new GradientStop(end, 1),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatSize(long bytes)
|
||||
{
|
||||
return bytes switch
|
||||
{
|
||||
>= 1L << 30 => $"{bytes / (double)(1L << 30):0.0} GiB",
|
||||
>= 1L << 20 => $"{bytes / (double)(1L << 20):0.0} MiB",
|
||||
>= 1L << 10 => $"{bytes / (double)(1L << 10):0.0} KiB",
|
||||
_ => $"{bytes} B",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -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,94 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public sealed class GuiSettings
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
public List<string> GameFolders { get; set; } = new();
|
||||
|
||||
/// <summary>Eboot paths hidden from the library via "Remove from library".</summary>
|
||||
public List<string> ExcludedGames { get; set; } = new();
|
||||
|
||||
public string LogLevel { get; set; } = "Info";
|
||||
|
||||
public int ImportTraceLimit { get; set; }
|
||||
|
||||
public bool StrictDynlibResolution { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
||||
/// </summary>
|
||||
public bool LogToFile { get; set; }
|
||||
|
||||
/// <summary>If <see cref="LogToFile"/> is true it logs to this file path.</summary>
|
||||
public string? LogFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="OverrideLogFile"/> is false it appends <titleId>-<timestamp> to the filename specified by
|
||||
/// <see cref="LogFilePath"/>. Otherwise it uses the exact filename from <see cref="LogFilePath"/>
|
||||
/// </summary>
|
||||
public bool OverrideLogFile { get; set; }
|
||||
|
||||
/// <summary>Loop the selected game's sce_sys/snd0.at9 preview music.</summary>
|
||||
public bool PlayTitleMusic { get; set; } = true;
|
||||
|
||||
public string? EmulatorPath { get; set; }
|
||||
|
||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||
public bool DiscordRichPresence { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Discord application ID used for Rich Presence; the default is the
|
||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||
/// "Playing …" (register at discord.com/developers/applications).
|
||||
/// </summary>
|
||||
public string DiscordClientId { get; set; } = "1525606762248540221";
|
||||
|
||||
// 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,289 +0,0 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="SharpEmu.GUI.MainWindow"
|
||||
Title="SharpEmu"
|
||||
Width="1280" Height="820"
|
||||
MinWidth="980" MinHeight="640"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="{StaticResource BgBrush}"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="PreferSystemChrome"
|
||||
ExtendClientAreaTitleBarHeightHint="44"
|
||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
|
||||
KeyDown="OnKeyDown">
|
||||
|
||||
<Grid RowDefinitions="44,*,32">
|
||||
|
||||
<!-- Selected-game backdrop: key art behind the main content, dimmed by
|
||||
a scrim so tiles and text stay readable. Fades on selection. -->
|
||||
<Panel Grid.Row="1" ClipToBounds="True">
|
||||
<Image x:Name="BackdropImage" Stretch="UniformToFill" Opacity="0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Image.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Opacity" Duration="0:0:0.35" />
|
||||
</Transitions>
|
||||
</Image.Transitions>
|
||||
</Image>
|
||||
<Border>
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
|
||||
<GradientStop Offset="0" Color="#730D1017" />
|
||||
<GradientStop Offset="0.55" Color="#BF0D1017" />
|
||||
<GradientStop Offset="1" Color="#EB0D1017" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- Title bar -->
|
||||
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
<Border Classes="pill">
|
||||
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Main content -->
|
||||
<Grid Grid.Row="1" Margin="18,14,18,14" RowDefinitions="Auto,*,Auto,Auto">
|
||||
|
||||
<!-- Library toolbar -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,Auto,*,Auto,Auto,Auto,Auto" Margin="6,0,6,12">
|
||||
<TextBlock Grid.Column="0" Text="Library" FontSize="22" FontWeight="Bold" VerticalAlignment="Center" />
|
||||
<Border Grid.Column="1" Classes="pill" Margin="12,2,0,0" VerticalAlignment="Center">
|
||||
<TextBlock x:Name="GameCountText" Text="0 games" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<TextBox Grid.Column="3" x:Name="SearchBox" Watermark="Search library…" Width="280"
|
||||
VerticalAlignment="Center" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="4" x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="5" x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan"
|
||||
VerticalAlignment="Center" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="6" x:Name="OpenFileButton" Classes="ghost" Content="Open file…"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<!-- Cover-art grid -->
|
||||
<Panel Grid.Row="1">
|
||||
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
|
||||
SelectionMode="Single" Padding="0">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
||||
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
||||
Foreground="{StaticResource DangerHoverBrush}">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Width="128" Height="186" Spacing="7">
|
||||
<Border Classes="coverShadow" Width="128" Height="128">
|
||||
<Border Classes="coverClip">
|
||||
<Panel>
|
||||
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
|
||||
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
|
||||
Foreground="#E8ECF4" Opacity="0.85"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
</Border>
|
||||
<StackPanel Spacing="2">
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock Text="{Binding Detail}" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="False">
|
||||
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
|
||||
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
||||
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
||||
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
<!-- Console (collapsible) -->
|
||||
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
|
||||
Margin="0,12,0,0" IsVisible="False">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto, Auto" Margin="16,12,16,8">
|
||||
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
|
||||
Watermark="Search..." MaxWidth="5500" />
|
||||
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
||||
FontSize="12" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="3" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
||||
Padding="10,4" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="4" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
||||
Padding="10,4" />
|
||||
</Grid>
|
||||
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
||||
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,12,12">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Launch bar -->
|
||||
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14">
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- Selected game cover thumbnail -->
|
||||
<Border Grid.Column="0" Classes="coverClip" Width="56" Height="56" CornerRadius="8"
|
||||
VerticalAlignment="Center">
|
||||
<Panel x:Name="SelectedCoverPanel">
|
||||
<Border Background="{Binding PlaceholderBrush, FallbackValue={x:Null}}"
|
||||
IsVisible="{Binding !HasCover, FallbackValue=False}">
|
||||
<TextBlock Text="{Binding Initials}" FontSize="20" FontWeight="Bold"
|
||||
Foreground="#E8ECF4" Opacity="0.85"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Image Source="{Binding Cover}" Stretch="UniformToFill"
|
||||
IsVisible="{Binding HasCover, FallbackValue=False}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="3" VerticalAlignment="Center" Margin="14,0,14,0">
|
||||
<TextBlock x:Name="SelectedGameTitle" Text="No game selected" FontSize="16" FontWeight="Bold"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
<Ellipse x:Name="StatusDot" Width="8" Height="8" Fill="{StaticResource FaintBrush}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="StatusText" Text="Idle" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<ToggleButton x:Name="OptionsToggle" Classes="ghost" Content="⚙ Options" />
|
||||
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
|
||||
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
||||
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Launch options (collapsible) -->
|
||||
<WrapPanel x:Name="OptionsPanel" IsVisible="False">
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="CPU engine" />
|
||||
<ComboBox x:Name="CpuEngineBox" Width="150" SelectedIndex="0" CornerRadius="8">
|
||||
<ComboBoxItem Content="Native" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Log level" />
|
||||
<ComboBox x:Name="LogLevelBox" Width="150" SelectedIndex="2" CornerRadius="8">
|
||||
<ComboBoxItem Content="Trace" />
|
||||
<ComboBoxItem Content="Debug" />
|
||||
<ComboBoxItem Content="Info" />
|
||||
<ComboBoxItem Content="Warning" />
|
||||
<ComboBoxItem Content="Error" />
|
||||
<ComboBoxItem Content="Critical" />
|
||||
</ComboBox>
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Import trace limit (0 = off)" />
|
||||
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
||||
Value="0" FormatString="0" CornerRadius="8" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
||||
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Log to file" />
|
||||
<ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Log file path" />
|
||||
<Button x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Override log file" />
|
||||
<ToggleSwitch x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
<StackPanel Margin="0,0,24,0">
|
||||
<TextBlock Classes="fieldLabel" Text="Title music" />
|
||||
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" IsChecked="True" />
|
||||
</StackPanel>
|
||||
<StackPanel>
|
||||
<TextBlock Classes="fieldLabel" Text="Discord presence" />
|
||||
<ToggleSwitch x:Name="DiscordToggle" OnContent="On" OffContent="Off" />
|
||||
</StackPanel>
|
||||
</WrapPanel>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Status bar -->
|
||||
<Grid Grid.Row="2" Background="{StaticResource ChromeBrush}" ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
|
||||
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock x:Name="StatusBarRight" Grid.Column="1" Text="" FontSize="11"
|
||||
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,44 +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>
|
||||
|
||||
<!-- Dependency-free; provides the BuildInfo provenance shown in the
|
||||
title bar. -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Avalonia.Desktop" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" />
|
||||
<PackageReference Include="Tmds.DBus.Protocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||
with the emulator's pad HLE. They are dependency-free, so they are
|
||||
compiled in directly rather than pulling a reference to all of
|
||||
SharpEmu.Libs into the launcher. -->
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\PadState.cs" Link="Input/PadState.cs" />
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -1,301 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using LibAtrac9;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Loops a game's sce_sys/snd0.at9 preview music while the game is selected
|
||||
/// in the library, like the console home screen. The ATRAC9 stream is decoded
|
||||
/// to WAV on a background task (vendored LibAtrac9); playback uses winmm's
|
||||
/// PlaySound, so this is Windows-only and a no-op elsewhere.
|
||||
/// </summary>
|
||||
internal sealed class SndPreviewPlayer
|
||||
{
|
||||
private const uint SND_ASYNC = 0x0001;
|
||||
private const uint SND_NODEFAULT = 0x0002;
|
||||
private const uint SND_MEMORY = 0x0004;
|
||||
private const uint SND_LOOP = 0x0008;
|
||||
|
||||
private readonly object _sync = new();
|
||||
private int _generation;
|
||||
private GCHandle _pinnedWav;
|
||||
private bool _playing;
|
||||
private bool _paused;
|
||||
private string? _cachedPath;
|
||||
private byte[]? _cachedWav;
|
||||
|
||||
/// <summary>Starts looping the given snd0.at9 after a short debounce.</summary>
|
||||
public void Play(string at9Path)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int generation;
|
||||
lock (_sync)
|
||||
{
|
||||
generation = ++_generation;
|
||||
}
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// Debounce so skimming through the library does not decode (or
|
||||
// start) a preview per tile.
|
||||
await Task.Delay(300).ConfigureAwait(false);
|
||||
|
||||
byte[]? wav;
|
||||
lock (_sync)
|
||||
{
|
||||
if (generation != _generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
wav = string.Equals(_cachedPath, at9Path, StringComparison.OrdinalIgnoreCase)
|
||||
? _cachedWav
|
||||
: null;
|
||||
}
|
||||
|
||||
if (wav is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
wav = DecodeAt9ToWav(File.ReadAllBytes(at9Path));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return; // corrupt or unsupported preview: stay silent
|
||||
}
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (generation != _generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cachedPath = at9Path;
|
||||
_cachedWav = wav;
|
||||
StopLocked();
|
||||
|
||||
// The WAV image must stay pinned while winmm plays from it.
|
||||
_pinnedWav = GCHandle.Alloc(wav, GCHandleType.Pinned);
|
||||
_playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT);
|
||||
if (!_playing)
|
||||
{
|
||||
_pinnedWav.Free();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
_generation++;
|
||||
StopLocked();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Silences playback but keeps the decoded track ready, so
|
||||
/// <see cref="Resume"/> can restart it (winmm cannot truly pause).
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_playing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = PlaySound(0, 0, 0);
|
||||
_playing = false;
|
||||
_paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Restarts the track silenced by <see cref="Pause"/>.</summary>
|
||||
public void Resume()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_paused || !_pinnedWav.IsAllocated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_paused = false;
|
||||
_playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopLocked()
|
||||
{
|
||||
_ = PlaySound(0, 0, 0);
|
||||
_playing = false;
|
||||
_paused = false;
|
||||
if (_pinnedWav.IsAllocated)
|
||||
{
|
||||
_pinnedWav.Free();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Guid Atrac9SubFormat = new("47E142D2-36BA-4D8D-88FC-61654F8C836C");
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a Sony AT9 (RIFF-wrapped ATRAC9) file to a PCM16 WAV image.
|
||||
/// Layout per Sony's container: an extensible fmt chunk whose extension
|
||||
/// carries the 4-byte codec config, a fact chunk with the sample count
|
||||
/// and encoder delay, and superframes in the data chunk.
|
||||
/// </summary>
|
||||
private static byte[] DecodeAt9ToWav(byte[] file)
|
||||
{
|
||||
using var reader = new BinaryReader(new MemoryStream(file), Encoding.ASCII);
|
||||
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF")
|
||||
{
|
||||
throw new InvalidDataException("Not a RIFF file.");
|
||||
}
|
||||
|
||||
reader.BaseStream.Position += 4; // riff size
|
||||
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE")
|
||||
{
|
||||
throw new InvalidDataException("Not a WAVE file.");
|
||||
}
|
||||
|
||||
byte[]? configData = null;
|
||||
var sampleCount = 0;
|
||||
var encoderDelay = 0;
|
||||
var dataOffset = -1;
|
||||
var dataSize = 0;
|
||||
|
||||
while (reader.BaseStream.Position + 8 <= reader.BaseStream.Length)
|
||||
{
|
||||
var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4));
|
||||
var chunkSize = reader.ReadInt32();
|
||||
var chunkStart = reader.BaseStream.Position;
|
||||
switch (chunkId)
|
||||
{
|
||||
case "fmt ":
|
||||
var formatTag = reader.ReadUInt16();
|
||||
reader.BaseStream.Position = chunkStart + 24; // to SubFormat GUID
|
||||
var subFormat = new Guid(reader.ReadBytes(16));
|
||||
if (formatTag != 0xFFFE || subFormat != Atrac9SubFormat)
|
||||
{
|
||||
throw new InvalidDataException("Not an ATRAC9 stream.");
|
||||
}
|
||||
|
||||
reader.BaseStream.Position += 4; // version info
|
||||
configData = reader.ReadBytes(4);
|
||||
break;
|
||||
case "fact":
|
||||
sampleCount = reader.ReadInt32();
|
||||
reader.BaseStream.Position += 4; // input overlap delay
|
||||
encoderDelay = reader.ReadInt32();
|
||||
break;
|
||||
case "data":
|
||||
dataOffset = (int)chunkStart;
|
||||
dataSize = chunkSize;
|
||||
break;
|
||||
}
|
||||
|
||||
reader.BaseStream.Position = chunkStart + chunkSize + (chunkSize & 1);
|
||||
}
|
||||
|
||||
if (configData is null || sampleCount <= 0 || dataOffset < 0)
|
||||
{
|
||||
throw new InvalidDataException("Missing fmt, fact, or data chunk.");
|
||||
}
|
||||
|
||||
var decoder = new Atrac9Decoder();
|
||||
decoder.Initialize(configData);
|
||||
var config = decoder.Config;
|
||||
|
||||
var superframeCount = (sampleCount + encoderDelay + config.SuperframeSamples - 1) / config.SuperframeSamples;
|
||||
superframeCount = Math.Min(superframeCount, dataSize / config.SuperframeBytes);
|
||||
|
||||
var channels = config.ChannelCount;
|
||||
var pcmBuffer = new short[channels][];
|
||||
for (var i = 0; i < channels; i++)
|
||||
{
|
||||
pcmBuffer[i] = new short[config.SuperframeSamples];
|
||||
}
|
||||
|
||||
var wav = new byte[44 + (sampleCount * channels * 2)];
|
||||
WriteWavHeader(wav, channels, config.SampleRate, sampleCount);
|
||||
|
||||
var superframe = new byte[config.SuperframeBytes];
|
||||
var decodedIndex = 0L; // per-channel, includes the encoder delay
|
||||
var written = 0;
|
||||
for (var f = 0; f < superframeCount && written < sampleCount; f++)
|
||||
{
|
||||
Buffer.BlockCopy(file, dataOffset + (f * config.SuperframeBytes), superframe, 0, config.SuperframeBytes);
|
||||
decoder.Decode(superframe, pcmBuffer);
|
||||
for (var s = 0; s < config.SuperframeSamples && written < sampleCount; s++)
|
||||
{
|
||||
if (decodedIndex++ < encoderDelay)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sampleOffset = 44 + ((long)written * channels * 2);
|
||||
for (var ch = 0; ch < channels; ch++)
|
||||
{
|
||||
BinaryPrimitives.WriteInt16LittleEndian(
|
||||
wav.AsSpan((int)(sampleOffset + (ch * 2))),
|
||||
pcmBuffer[ch][s]);
|
||||
}
|
||||
|
||||
written++;
|
||||
}
|
||||
}
|
||||
|
||||
return wav;
|
||||
}
|
||||
|
||||
private static void WriteWavHeader(byte[] wav, int channels, int sampleRate, int sampleCount)
|
||||
{
|
||||
var span = wav.AsSpan();
|
||||
Encoding.ASCII.GetBytes("RIFF").CopyTo(span);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(span[4..], wav.Length - 8);
|
||||
Encoding.ASCII.GetBytes("WAVE").CopyTo(span[8..]);
|
||||
Encoding.ASCII.GetBytes("fmt ").CopyTo(span[12..]);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(span[16..], 16);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(span[20..], 1); // PCM
|
||||
BinaryPrimitives.WriteInt16LittleEndian(span[22..], (short)channels);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(span[24..], sampleRate);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(span[28..], sampleRate * channels * 2);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(span[32..], (short)(channels * 2));
|
||||
BinaryPrimitives.WriteInt16LittleEndian(span[34..], 16); // bits per sample
|
||||
Encoding.ASCII.GetBytes("data").CopyTo(span[36..]);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(span[40..], sampleCount * channels * 2);
|
||||
}
|
||||
|
||||
[DllImport("winmm.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool PlaySound(nint sound, nint module, uint flags);
|
||||
}
|
||||
@@ -1,189 +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=="
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,25 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||
public sealed class CpuContext
|
||||
{
|
||||
private readonly ulong[] _registers = new ulong[16];
|
||||
private readonly ulong[] _xmmRegisters = new ulong[32];
|
||||
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
|
||||
private bool _raxWritten;
|
||||
|
||||
public ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||
public CpuContext(ICpuMemory memory, Generation generation)
|
||||
{
|
||||
Memory = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||
TargetGeneration = generation;
|
||||
}
|
||||
|
||||
public Generation TargetGeneration { get; } = generation;
|
||||
public ICpuMemory Memory { get; }
|
||||
|
||||
public Generation TargetGeneration { get; }
|
||||
|
||||
public ulong Rip { get; set; }
|
||||
|
||||
@@ -125,91 +130,6 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||
SetYmmUpper(registerIndex, highLow, highHigh);
|
||||
}
|
||||
|
||||
public bool TryReadByte(ulong address, out byte value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
if (!Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = buffer[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryReadUInt16(ulong address, out ushort value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||
if (!Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteUInt16(ulong address, ushort value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadInt32(ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteInt32(ulong address, int value, bool checkNil = false)
|
||||
{
|
||||
if (checkNil && address == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
public bool TryReadUInt32(ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteUInt32(ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryWriteInt64(ulong address, long value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(long)];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadUInt64(ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
@@ -230,33 +150,6 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadNullTerminatedUtf8(ulong address, int capacity, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (address == 0 || capacity <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytes[index] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool PushUInt64(ulong value)
|
||||
{
|
||||
var rsp = this[CpuRegister.Rsp];
|
||||
@@ -276,23 +169,4 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||
this[CpuRegister.Rsp] = rsp + sizeof(ulong);
|
||||
return true;
|
||||
}
|
||||
|
||||
public int SetReturn(int result, Type? cast = null)
|
||||
{
|
||||
var value = cast switch
|
||||
{
|
||||
null => (ulong)result,
|
||||
_ when cast == typeof(long) => (ulong)(long)result,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
this[CpuRegister.Rax] = unchecked(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
public int SetReturn(OrbisGen2Result result)
|
||||
{
|
||||
this[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -47,7 +47,6 @@ public sealed class ModuleManager : IModuleManager
|
||||
var handler = CreateHandler(type, method, instances);
|
||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -106,7 +105,6 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return false;
|
||||
@@ -114,13 +112,11 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if ((export.Target & context.TargetGeneration) == 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
context.ClearRaxWriteFlag();
|
||||
int ret = ((SysAbiFunction)function).Invoke(context);
|
||||
|
||||
|
||||
@@ -40,13 +40,6 @@ public enum OrbisGen2Result : int
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_DEADLOCK = unchecked((int)0x8002000B),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the waited-on object was deleted while the caller was
|
||||
/// blocked on it. Matches the SCE kernel EACCES code that waiters of a
|
||||
/// deleted semaphore observe.
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_DELETED = unchecked((int)0x8002000D),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the target resource is busy.
|
||||
/// </summary>
|
||||
@@ -57,13 +50,6 @@ public enum OrbisGen2Result : int
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_TRY_AGAIN = unchecked((int)0x80020023),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a blocked wait was canceled (e.g. sceKernelCancelSema).
|
||||
/// Matches the SCE kernel ECANCELED code that canceled semaphore waiters
|
||||
/// observe.
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_CANCELED = unchecked((int)0x80020055),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that behavior is recognized but not implemented yet.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,10 +8,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||
</ItemGroup>
|
||||
|
||||
+523
-674
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
@@ -17,9 +18,9 @@ internal static class Gen5ShaderMetadataReader
|
||||
out Gen5ShaderMetadata metadata)
|
||||
{
|
||||
metadata = default!;
|
||||
if (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
||||
if (!TryReadUInt64(ctx, shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
||||
userDataAddress == 0 ||
|
||||
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
|
||||
!TryReadUInt64(ctx, userDataAddress, out var directResourceOffsetsAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -27,7 +28,8 @@ internal static class Gen5ShaderMetadataReader
|
||||
var resourceOffsets = new ulong[ResourceClassCount];
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(
|
||||
if (!TryReadUInt64(
|
||||
ctx,
|
||||
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
|
||||
out resourceOffsets[resourceClass]))
|
||||
{
|
||||
@@ -35,9 +37,9 @@ internal static class Gen5ShaderMetadataReader
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt16(userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||
!ctx.TryReadUInt16(userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||
!ctx.TryReadUInt16(userDataAddress + 0x2C, out var directResourceCount) ||
|
||||
if (!TryReadUInt16(ctx, userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||
!TryReadUInt16(ctx, userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||
!TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount) ||
|
||||
directResourceCount > MaxMetadataEntries)
|
||||
{
|
||||
return false;
|
||||
@@ -46,7 +48,8 @@ internal static class Gen5ShaderMetadataReader
|
||||
var resourceCounts = new ushort[ResourceClassCount];
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
if (!ctx.TryReadUInt16(
|
||||
if (!TryReadUInt16(
|
||||
ctx,
|
||||
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
|
||||
out resourceCounts[resourceClass]) ||
|
||||
resourceCounts[resourceClass] > MaxMetadataEntries)
|
||||
@@ -65,7 +68,7 @@ internal static class Gen5ShaderMetadataReader
|
||||
|
||||
for (uint type = 0; type < directResourceCount; type++)
|
||||
{
|
||||
if (!ctx.TryReadUInt16(directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
|
||||
if (!TryReadUInt16(ctx, directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -93,7 +96,8 @@ internal static class Gen5ShaderMetadataReader
|
||||
|
||||
for (uint slot = 0; slot < count; slot++)
|
||||
{
|
||||
if (!ctx.TryReadUInt16(
|
||||
if (!TryReadUInt16(
|
||||
ctx,
|
||||
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
||||
out var sharp))
|
||||
{
|
||||
@@ -121,4 +125,30 @@ internal static class Gen5ShaderMetadataReader
|
||||
resources);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt16(CpuContext ctx, ulong address, out ushort value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using System.Numerics;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
@@ -1404,7 +1405,8 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
continue;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt32(
|
||||
if (!TryReadUInt32(
|
||||
ctx,
|
||||
address + (ulong)(index * sizeof(uint)),
|
||||
out var value) &&
|
||||
!TryReadUserDataScalarLoad(
|
||||
@@ -1725,4 +1727,16 @@ internal static class Gen5ShaderScalarEvaluator
|
||||
opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
|
||||
opcode.StartsWith("ImageGather", StringComparison.Ordinal);
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -294,7 +294,7 @@ internal static class Gen5ShaderTranslator
|
||||
var instructionCount = 0;
|
||||
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}";
|
||||
return false;
|
||||
@@ -317,7 +317,7 @@ internal static class Gen5ShaderTranslator
|
||||
words[0] = word;
|
||||
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}";
|
||||
return false;
|
||||
@@ -397,7 +397,7 @@ internal static class Gen5ShaderTranslator
|
||||
case 0x34:
|
||||
case 0x35:
|
||||
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}";
|
||||
return false;
|
||||
@@ -418,7 +418,7 @@ internal static class Gen5ShaderTranslator
|
||||
return DecodeFlat(word, out name, out sizeDwords, out error);
|
||||
case 0x38:
|
||||
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}";
|
||||
return false;
|
||||
@@ -427,7 +427,7 @@ internal static class Gen5ShaderTranslator
|
||||
return DecodeMubuf(word, mubufExtra, out name, out sizeDwords, out error);
|
||||
case 0x3A:
|
||||
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}";
|
||||
return false;
|
||||
@@ -1741,6 +1741,19 @@ internal static class Gen5ShaderTranslator
|
||||
counts[key] = count + 1;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private readonly record struct ShaderDecodeInfo(
|
||||
int InstructionCount,
|
||||
Dictionary<string, int> Counts,
|
||||
|
||||
@@ -1,102 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM condition.
|
||||
// AgcExports re-checks every waiter against guest memory on each submit and resumes
|
||||
// the ones whose condition became true (labels are advanced by ReleaseMem/WriteData/
|
||||
// DmaData packets or by direct CPU writes).
|
||||
internal static class GpuWaitRegistry
|
||||
{
|
||||
public struct WaitingDcb
|
||||
{
|
||||
public ulong CommandBufferAddress;
|
||||
public ulong ResumeAddress;
|
||||
public uint TotalDwords;
|
||||
public uint ResumeOffset;
|
||||
public ulong WaitAddress;
|
||||
public ulong ReferenceValue;
|
||||
public ulong Mask;
|
||||
public uint CompareFunction;
|
||||
public bool Is64Bit;
|
||||
public object? State;
|
||||
}
|
||||
|
||||
private static readonly object _gate = new();
|
||||
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
||||
|
||||
public static void Register(ulong address, WaitingDcb waiter)
|
||||
{
|
||||
waiter.WaitAddress = address;
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_waiters.TryGetValue(address, out var list))
|
||||
{
|
||||
list = new List<WaitingDcb>();
|
||||
_waiters.Add(address, list);
|
||||
}
|
||||
|
||||
list.Add(waiter);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluates every registered waiter. readValue receives (address, is64Bit) and
|
||||
// returns null when the memory is unreadable; such waiters are kept registered.
|
||||
public static List<WaitingDcb>? CollectSatisfied(Func<ulong, bool, ulong?> readValue)
|
||||
{
|
||||
List<WaitingDcb>? woken = null;
|
||||
lock (_gate)
|
||||
{
|
||||
List<ulong>? emptied = null;
|
||||
foreach (var (address, list) in _waiters)
|
||||
{
|
||||
for (var i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var value = readValue(address, list[i].Is64Bit);
|
||||
if (value is null || !Compare(list[i], value.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
woken ??= new List<WaitingDcb>();
|
||||
woken.Add(list[i]);
|
||||
list.RemoveAt(i);
|
||||
}
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
emptied ??= new List<ulong>();
|
||||
emptied.Add(address);
|
||||
}
|
||||
}
|
||||
|
||||
if (emptied != null)
|
||||
{
|
||||
foreach (var address in emptied)
|
||||
{
|
||||
_waiters.Remove(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return woken;
|
||||
}
|
||||
|
||||
public static bool Compare(in WaitingDcb waiter, ulong value)
|
||||
{
|
||||
var masked = value & waiter.Mask;
|
||||
return waiter.CompareFunction switch
|
||||
{
|
||||
1 => masked < waiter.ReferenceValue,
|
||||
2 => masked <= waiter.ReferenceValue,
|
||||
3 => masked == waiter.ReferenceValue,
|
||||
4 => masked != waiter.ReferenceValue,
|
||||
5 => masked >= waiter.ReferenceValue,
|
||||
6 => masked > waiter.ReferenceValue,
|
||||
// 0 is "always" in the PM4 encoding and 7 is reserved; treating both as
|
||||
// satisfied keeps a malformed packet from suspending its DCB forever.
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -267,53 +267,19 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ulong bytesRead = 0;
|
||||
|
||||
// Unregistered/missing files are zero-filled instead of failing: games queue
|
||||
// speculative reads and only consume the bytes on success paths.
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
|
||||
{
|
||||
if (destination != 0 && size > 0)
|
||||
{
|
||||
int chunkSize = (int)Math.Min(size, 4096);
|
||||
Span<byte> zeros = stackalloc byte[chunkSize];
|
||||
zeros.Clear();
|
||||
while (bytesRead < size)
|
||||
{
|
||||
int currentChunk = (int)Math.Min((ulong)chunkSize, size - bytesRead);
|
||||
if (!ctx.Memory.TryWrite(destination + bytesRead, zeros[..currentChunk]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += (ulong)currentChunk;
|
||||
}
|
||||
}
|
||||
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, "(missing)", (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
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;
|
||||
}
|
||||
|
||||
// Offset -1 means "continue after the previous read of this file id".
|
||||
if (fileOffset == unchecked((ulong)(long)-1))
|
||||
{
|
||||
fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size);
|
||||
}
|
||||
else if (fileOffset > long.MaxValue)
|
||||
{
|
||||
fileOffset = 0;
|
||||
}
|
||||
|
||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out bytesRead);
|
||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
|
||||
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
|
||||
|
||||
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -487,7 +453,7 @@ public static class AmprExports
|
||||
var offset = 0UL;
|
||||
while (offset < writeOffset)
|
||||
{
|
||||
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
|
||||
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -917,6 +883,19 @@ public static class AmprExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
||||
{
|
||||
if (!_traceAmpr)
|
||||
|
||||
@@ -1,156 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Ampr;
|
||||
|
||||
/// <summary>
|
||||
/// sceAmprAprCommandBufferReadFile carries -1 instead of an absolute offset whenever the guest
|
||||
/// wants "the next sequential chunk" of a file. For an id Software PACK archive that sequence is
|
||||
/// always header -> directory table -> individual archived files, and the guest never tells us
|
||||
/// which archived file it wants next - only the byte count it expects, taken from the very
|
||||
/// directory entry it already parsed on its side. This tracks a per-fileId read cursor, and once
|
||||
/// the directory table has streamed past, parses it so later reads can be matched back to their
|
||||
/// real absolute offset by the size the guest asks for.
|
||||
/// </summary>
|
||||
internal static class PakDirectoryTracker
|
||||
{
|
||||
private const int DirectoryEntrySize = 64;
|
||||
private const int DirectoryEntryNameLength = 56;
|
||||
|
||||
private static readonly bool _trace =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal) ||
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
|
||||
|
||||
private sealed class DirectoryEntry(string name, uint filePos, uint fileLen)
|
||||
{
|
||||
public string Name { get; } = name;
|
||||
public uint FilePos { get; } = filePos;
|
||||
public uint FileLen { get; } = fileLen;
|
||||
public bool Consumed { get; set; }
|
||||
}
|
||||
|
||||
private sealed class FileState
|
||||
{
|
||||
public ulong NextOffset;
|
||||
public bool ExpectingDirectory;
|
||||
public List<DirectoryEntry>? Directory;
|
||||
}
|
||||
|
||||
private static readonly ConcurrentDictionary<uint, FileState> _state = new();
|
||||
|
||||
/// <summary>
|
||||
/// Resolves what "read the next sequential chunk of size <paramref name="requestedSize"/>"
|
||||
/// really means for this fileId: an unconsumed directory entry whose length matches, if the
|
||||
/// directory has already been parsed, otherwise the tracked linear cursor.
|
||||
/// </summary>
|
||||
public static ulong ResolveSequentialOffset(uint fileId, ulong requestedSize)
|
||||
{
|
||||
var state = _state.GetOrAdd(fileId, static _ => new FileState());
|
||||
|
||||
if (state.Directory is { } entries)
|
||||
{
|
||||
foreach (var entry in entries)
|
||||
{
|
||||
if (!entry.Consumed && entry.FileLen == requestedSize)
|
||||
{
|
||||
entry.Consumed = true;
|
||||
if (_trace)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pak.directory_match: id=0x{fileId:X8} name='{entry.Name}' " +
|
||||
$"filepos=0x{entry.FilePos:X8} filelen=0x{entry.FileLen:X8}");
|
||||
}
|
||||
|
||||
return entry.FilePos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return state.NextOffset;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Feeds back a completed read so the tracker can advance its cursor, recognize a PACK
|
||||
/// header and jump straight to its directory table, or parse the directory table itself
|
||||
/// once it streams past.
|
||||
/// </summary>
|
||||
public static void OnReadCompleted(CpuContext ctx, uint fileId, ulong destination, ulong fileOffset, ulong bytesRead)
|
||||
{
|
||||
if (bytesRead == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var state = _state.GetOrAdd(fileId, static _ => new FileState());
|
||||
|
||||
if (state.ExpectingDirectory && fileOffset == state.NextOffset)
|
||||
{
|
||||
state.Directory = TryParseDirectory(ctx, destination, bytesRead);
|
||||
state.ExpectingDirectory = false;
|
||||
state.NextOffset = fileOffset + bytesRead;
|
||||
if (_trace)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] pak.directory_parsed: id=0x{fileId:X8} entries={state.Directory?.Count ?? 0}");
|
||||
}
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
if (fileOffset == 0 && bytesRead >= 12 && TryReadPackHeader(ctx, destination, out var dirOffset))
|
||||
{
|
||||
state.NextOffset = dirOffset;
|
||||
state.ExpectingDirectory = true;
|
||||
return;
|
||||
}
|
||||
|
||||
state.NextOffset = fileOffset + bytesRead;
|
||||
}
|
||||
|
||||
private static bool TryReadPackHeader(CpuContext ctx, ulong destination, out ulong dirOffset)
|
||||
{
|
||||
dirOffset = 0;
|
||||
Span<byte> header = stackalloc byte[12];
|
||||
if (!ctx.Memory.TryRead(destination, header) ||
|
||||
header[0] != (byte)'P' || header[1] != (byte)'A' || header[2] != (byte)'C' || header[3] != (byte)'K')
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
dirOffset = BinaryPrimitives.ReadUInt32LittleEndian(header[4..8]);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static List<DirectoryEntry>? TryParseDirectory(CpuContext ctx, ulong destination, ulong length)
|
||||
{
|
||||
var count = (int)(length / DirectoryEntrySize);
|
||||
if (count <= 0)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var entries = new List<DirectoryEntry>(count);
|
||||
Span<byte> record = stackalloc byte[DirectoryEntrySize];
|
||||
for (var i = 0; i < count; i++)
|
||||
{
|
||||
if (!ctx.Memory.TryRead(destination + (ulong)(i * DirectoryEntrySize), record))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
var nameBytes = record[..DirectoryEntryNameLength];
|
||||
var nullIndex = nameBytes.IndexOf((byte)0);
|
||||
var name = Encoding.ASCII.GetString(nullIndex >= 0 ? nameBytes[..nullIndex] : nameBytes);
|
||||
var filePos = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(56, 4));
|
||||
var fileLen = BinaryPrimitives.ReadUInt32LittleEndian(record.Slice(60, 4));
|
||||
entries.Add(new DirectoryEntry(name, filePos, fileLen));
|
||||
}
|
||||
|
||||
return entries;
|
||||
}
|
||||
}
|
||||
@@ -73,7 +73,7 @@ public static class AppContentExports
|
||||
var valueAddress = ctx[CpuRegister.Rsi];
|
||||
if (valueAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
int value;
|
||||
@@ -83,18 +83,18 @@ public static class AppContentExports
|
||||
}
|
||||
else if (!TryReadUserDefinedParam(paramId, out value))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||
if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceAppContent($"app_param_get_int id={paramId} value={value}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -168,6 +168,12 @@ public static class AppContentExports
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
private static void TraceAppContent(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -3,19 +3,15 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class AudioOut2Exports
|
||||
{
|
||||
// Sized from guest evidence, not SDK headers: Quake keeps its
|
||||
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
|
||||
// and an earlier 0x80-byte ResetParam write zeroed that canary
|
||||
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
|
||||
// the whole audio init). Stay well below 0x60 and only write the prefix we
|
||||
// populate.
|
||||
private const int AudioOut2ContextParamSize = 0x30;
|
||||
private const int AudioOut2ContextParamSize = 0x80;
|
||||
private const int AudioOut2ContextMemorySize = 0x10000;
|
||||
private const int AudioOut2ContextMemoryAlignment = 0x10000;
|
||||
private static long _nextContextHandle = 1;
|
||||
private static long _nextUserHandle = 1;
|
||||
private static int _nextPortId;
|
||||
@@ -41,7 +37,7 @@ public static class AudioOut2Exports
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||
@@ -52,8 +48,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
||||
|
||||
return ctx.Memory.TryWrite(paramAddress, param)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -64,15 +60,22 @@ public static class AudioOut2Exports
|
||||
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || outMemorySizeAddress == 0)
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
||||
Span<byte> memoryInfo = stackalloc byte[0x20];
|
||||
memoryInfo.Clear();
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
||||
|
||||
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -88,13 +91,13 @@ public static class AudioOut2Exports
|
||||
var outContextAddress = ctx[CpuRegister.Rcx];
|
||||
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return TryWriteUInt64(ctx, outContextAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -102,7 +105,7 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2ContextDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JK2wamZPzwM",
|
||||
@@ -117,14 +120,14 @@ public static class AudioOut2Exports
|
||||
var contextAddress = ctx[CpuRegister.Rcx];
|
||||
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
||||
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
||||
return ctx.TryWriteUInt64(outPortAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return TryWriteUInt64(ctx, outPortAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -138,7 +141,7 @@ public static class AudioOut2Exports
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle == 0 || stateAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var type = (int)((handle >> 16) & 0xFF);
|
||||
@@ -151,8 +154,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||
|
||||
return ctx.Memory.TryWrite(stateAddress, state)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -165,7 +168,7 @@ public static class AudioOut2Exports
|
||||
var infoAddress = ctx[CpuRegister.Rdi];
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> info = stackalloc byte[0x40];
|
||||
@@ -175,8 +178,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
||||
|
||||
return ctx.Memory.TryWrite(infoAddress, info)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -184,14 +187,14 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2PortDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "IaZXJ9M79uo",
|
||||
ExportName = "sceAudioOut2UserDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xywYcRB7nbQ",
|
||||
@@ -204,12 +207,25 @@ public static class AudioOut2Exports
|
||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||
return ctx.TryWriteUInt64(outUserAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return TryWriteUInt64(ctx, outUserAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ using SharpEmu.HLE;
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Diagnostics;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
@@ -83,7 +84,7 @@ public static class AudioOutExports
|
||||
ExportName = "sceAudioOutInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut")]
|
||||
public static int AudioOutInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||
public static int AudioOutInit(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ekNvsT22rsY",
|
||||
@@ -100,7 +101,7 @@ public static class AudioOutExports
|
||||
if (bufferLength == 0 || frequency == 0 ||
|
||||
!TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
WinMmAudioPort? backend = null;
|
||||
@@ -132,7 +133,7 @@ public static class AudioOutExports
|
||||
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
|
||||
$"{channels} ch, {(isFloat ? "float32" : "s16")}, " +
|
||||
$"{bufferLength} frames, backend={backendName}");
|
||||
return ctx.SetReturn(handle);
|
||||
return SetReturn(ctx, handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -145,11 +146,11 @@ public static class AudioOutExports
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!Ports.TryRemove(handle, out var port))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
port.Dispose();
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -163,12 +164,12 @@ public static class AudioOutExports
|
||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||
if (!Ports.TryGetValue(handle, out var port))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (sourceAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
|
||||
@@ -177,7 +178,7 @@ public static class AudioOutExports
|
||||
var source = buffer.AsSpan(0, port.BufferByteLength);
|
||||
if (!ctx.Memory.TryRead(sourceAddress, source))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (port.Backend is null ||
|
||||
@@ -191,7 +192,7 @@ public static class AudioOutExports
|
||||
port.PaceSilence();
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -207,12 +208,19 @@ public static class AudioOutExports
|
||||
public static int AudioOutSetVolume(CpuContext ctx)
|
||||
{
|
||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return ctx.SetReturn(
|
||||
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,
|
||||
|
||||
@@ -35,42 +35,6 @@ public static class AvPlayerExports
|
||||
return unchecked((int)handle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JdksQu8pNdQ",
|
||||
ExportName = "sceAvPlayerGetVideoDataEx",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerGetVideoDataEx(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// "UbQoYawOsfY" is sceAvPlayerIsActive, not sceAvPlayerGetVideoDataEx (the previous NID here
|
||||
// was wrong - verified by hashing both names against scripts/ps5_names.txt). No player is
|
||||
// ever actually active in this HLE implementation, so report false/inactive.
|
||||
[SysAbiExport(
|
||||
Nid = "UbQoYawOsfY",
|
||||
ExportName = "sceAvPlayerIsActive",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerIsActive(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Wnp1OVcrZgk",
|
||||
ExportName = "sceAvPlayerGetAudioData",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceAvPlayer")]
|
||||
public static int AvPlayerGetAudioData(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "HD1YKVU26-M",
|
||||
ExportName = "sceAvPlayerPostInit",
|
||||
@@ -82,7 +46,8 @@ public static class AvPlayerExports
|
||||
var dataAddress = ctx[CpuRegister.Rsi];
|
||||
lock (StateGate)
|
||||
{
|
||||
return ctx.SetReturn(
|
||||
return SetReturn(
|
||||
ctx,
|
||||
handle != 0 && dataAddress != 0 && Players.Contains(handle)
|
||||
? 0
|
||||
: InvalidParameters);
|
||||
@@ -98,7 +63,15 @@ public static class AvPlayerExports
|
||||
{
|
||||
lock (StateGate)
|
||||
{
|
||||
return ctx.SetReturn(Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
|
||||
return SetReturn(
|
||||
ctx,
|
||||
Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ namespace SharpEmu.Libs.CommonDialog;
|
||||
|
||||
public static class CommonDialogExports
|
||||
{
|
||||
private const int AlreadySystemInitialized = unchecked((int)0x80B80002);
|
||||
private static int _initialized;
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -17,12 +18,11 @@ public static class CommonDialogExports
|
||||
LibraryName = "libSceCommonDialog")]
|
||||
public static int CommonDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
// Games can initialize the common-dialog service defensively from more than one
|
||||
// subsystem. Keeping this HLE initialization idempotent avoids turning an error
|
||||
// reporting path into a second, unrelated failure.
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
||||
? 0
|
||||
: AlreadySystemInitialized;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
|
||||
@@ -8,13 +8,8 @@ namespace SharpEmu.Libs.CommonDialog;
|
||||
|
||||
public static class MsgDialogExports
|
||||
{
|
||||
private const int StatusNone = 0;
|
||||
private const int StatusInitialized = 1;
|
||||
private const int StatusFinished = 3;
|
||||
private const int ResultSize = 0x20;
|
||||
|
||||
private const int AlreadyInitialized = unchecked((int)0x80B80002);
|
||||
private static int _initialized;
|
||||
private static int _status;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lDqxaY1UbEo",
|
||||
@@ -23,162 +18,9 @@ public static class MsgDialogExports
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
// Treat repeated initialization as success. The dialog service is process-global in
|
||||
// this HLE implementation and has no per-call resources to recreate.
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
Interlocked.Exchange(ref _status, StatusInitialized);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ePw-kqZmelo",
|
||||
ExportName = "sceMsgDialogTerminate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogTerminate(CpuContext ctx)
|
||||
{
|
||||
Interlocked.Exchange(ref _initialized, 0);
|
||||
Interlocked.Exchange(ref _status, StatusNone);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "b06Hh0DPEaE",
|
||||
ExportName = "sceMsgDialogOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogOpen(CpuContext ctx)
|
||||
{
|
||||
LogDialogMessage(ctx, ctx[CpuRegister.Rdi]);
|
||||
|
||||
// There is no host popup to actually show. Complete immediately with "finished" so a
|
||||
// guest polling loop (GetStatus/UpdateStatus -> GetResult -> Close) sees a dismissed
|
||||
// dialog on its very first poll instead of spinning forever waiting for user input.
|
||||
Interlocked.Exchange(ref _status, StatusFinished);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Best-effort extraction of the dialog text so fatal-error popups are visible in the
|
||||
// log even though no host dialog is shown. The PS5 SceMsgDialogParam layout is not
|
||||
// fully known, so chase every qword in the struct that points at readable text - one
|
||||
// level deep, then a second level for nested sub-param structs.
|
||||
private static void LogDialogMessage(CpuContext ctx, ulong paramAddress)
|
||||
{
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] sceMsgDialogOpen: param=NULL");
|
||||
return;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][INFO] sceMsgDialogOpen: param=0x{paramAddress:X12}");
|
||||
|
||||
Span<byte> head = stackalloc byte[0xA0];
|
||||
if (ctx.Memory.TryRead(paramAddress, head))
|
||||
{
|
||||
DumpPointerStrings(ctx, paramAddress, head, "param", chaseNested: true);
|
||||
}
|
||||
}
|
||||
|
||||
private static void DumpPointerStrings(CpuContext ctx, ulong baseAddress, ReadOnlySpan<byte> bytes, string label, bool chaseNested)
|
||||
{
|
||||
Span<byte> nested = stackalloc byte[0x40];
|
||||
for (var offset = 0; offset + 8 <= bytes.Length; offset += 8)
|
||||
{
|
||||
var value = System.Buffers.Binary.BinaryPrimitives.ReadUInt64LittleEndian(bytes[offset..]);
|
||||
if (value < 0x10000 || value == baseAddress)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var text = TryReadPrintableText(ctx, value);
|
||||
if (text is not null)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] {label}+0x{offset:X2} -> 0x{value:X12} text=\"{text}\"");
|
||||
}
|
||||
else if (chaseNested && ctx.Memory.TryRead(value, nested))
|
||||
{
|
||||
DumpPointerStrings(ctx, value, nested, $"{label}+0x{offset:X2}", chaseNested: false);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static string? TryReadPrintableText(CpuContext ctx, ulong address)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[256];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var nullIndex = bytes.IndexOf((byte)0);
|
||||
if (nullIndex < 2)
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
var candidate = bytes[..nullIndex];
|
||||
foreach (var b in candidate)
|
||||
{
|
||||
if (b is < 0x20 or > 0x7E && b != (byte)'\n' && b != (byte)'\r' && b != (byte)'\t')
|
||||
{
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
return System.Text.Encoding.ASCII.GetString(candidate).Replace("\n", "\\n").Replace("\r", "\\r");
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "CWVW78Qc3fI",
|
||||
ExportName = "sceMsgDialogGetStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogGetStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "6fIC3XKt2k0",
|
||||
ExportName = "sceMsgDialogUpdateStatus",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogUpdateStatus(CpuContext ctx) => SetReturn(ctx, Volatile.Read(ref _status));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Lr8ovHH9l6A",
|
||||
ExportName = "sceMsgDialogGetResult",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogGetResult(CpuContext ctx)
|
||||
{
|
||||
var resultAddress = ctx[CpuRegister.Rdi];
|
||||
if (resultAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> result = stackalloc byte[ResultSize];
|
||||
result.Clear();
|
||||
if (!ctx.Memory.TryWrite(resultAddress, result))
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "HTrcDKlFKuM",
|
||||
ExportName = "sceMsgDialogClose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMsgDialog")]
|
||||
public static int MsgDialogClose(CpuContext ctx)
|
||||
{
|
||||
Interlocked.Exchange(ref _status, StatusFinished);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
||||
? 0
|
||||
: AlreadyInitialized;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
@@ -93,7 +93,7 @@ public static class CxaGuardExports
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "9rAeANT2tyE",
|
||||
Nid = "",
|
||||
ExportName = "__cxa_guard_release",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
@@ -142,7 +142,7 @@ public static class CxaGuardExports
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "2emaaluWzUw",
|
||||
Nid = "",
|
||||
ExportName = "__cxa_guard_abort",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
|
||||
@@ -1,104 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.DiscMap;
|
||||
|
||||
/// <summary>
|
||||
/// HLE implementation of libSceDiscMap, ported from Kyty's LibDiscMap.cpp
|
||||
/// (https://github.com/InoriRus/Kyty, MIT). Titles installed to internal
|
||||
/// storage query these entry points on nearly every file access to decide
|
||||
/// whether a read must be redirected to the Blu-ray drive. Reporting every
|
||||
/// request as already resident on HDD lets the regular file system path
|
||||
/// service the read.
|
||||
/// </summary>
|
||||
public static class DiscMapExports
|
||||
{
|
||||
private const int DiscMapErrorInvalidArgument = unchecked((int)0x81100001);
|
||||
private const int DiscMapErrorLocationNotMapped = unchecked((int)0x81100002);
|
||||
private const int DiscMapErrorFileNotFound = unchecked((int)0x81100003);
|
||||
private const int DiscMapErrorNoBitmapInfo = unchecked((int)0x81100004);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lbQKqsERhtE",
|
||||
ExportName = "sceDiscMapIsRequestOnHDD",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceDiscMap")]
|
||||
public static int DiscMapIsRequestOnHDD(CpuContext ctx)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var offset = ctx[CpuRegister.Rsi];
|
||||
var size = ctx[CpuRegister.Rdx];
|
||||
var resultAddress = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (pathAddress == 0 || resultAddress == 0)
|
||||
{
|
||||
return DiscMapErrorInvalidArgument;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteInt32(resultAddress, 1))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
TraceDiscMap(ctx, "sceDiscMapIsRequestOnHDD", pathAddress, offset, size);
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fJgP+wqifno",
|
||||
ExportName = "sceDiscMapUnknownFJgP",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceDiscMap")]
|
||||
public static int DiscMapUnknownFJgP(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownFJgP");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "ioKMruft1ek",
|
||||
ExportName = "sceDiscMapUnknownIoKM",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceDiscMap")]
|
||||
public static int DiscMapUnknownIoKM(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownIoKM");
|
||||
|
||||
private static int WriteMappingTriple(CpuContext ctx, string exportName)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var offset = ctx[CpuRegister.Rsi];
|
||||
var size = ctx[CpuRegister.Rdx];
|
||||
var flagsAddress = ctx[CpuRegister.Rcx];
|
||||
var outAddress1 = ctx[CpuRegister.R8];
|
||||
var outAddress2 = ctx[CpuRegister.R9];
|
||||
|
||||
if (pathAddress == 0 || flagsAddress == 0 || outAddress1 == 0 || outAddress2 == 0)
|
||||
{
|
||||
return DiscMapErrorInvalidArgument;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(flagsAddress, 0) ||
|
||||
!ctx.TryWriteUInt64(outAddress1, 0) ||
|
||||
!ctx.TryWriteUInt64(outAddress2, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
TraceDiscMap(ctx, exportName, pathAddress, offset, size);
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static void TraceDiscMap(CpuContext ctx, string exportName, ulong pathAddress, ulong offset, ulong size)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCMAP"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var path = ctx.TryReadNullTerminatedUtf8(pathAddress, 1024, out var value)
|
||||
? value
|
||||
: $"<unreadable 0x{pathAddress:X16}>";
|
||||
Console.Error.WriteLine($"[HLE][DISCMAP] {exportName} path={path} offset=0x{offset:X} size=0x{size:X}");
|
||||
}
|
||||
}
|
||||
@@ -104,17 +104,17 @@ public static class FiberExports
|
||||
var optParam = ctx[CpuRegister.Rdi];
|
||||
if (optParam == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((optParam & 7) != 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
return SetReturn(ctx, FiberErrorAlignment);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -127,24 +127,24 @@ public static class FiberExports
|
||||
var fiber = ctx[CpuRegister.Rdi];
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
return SetReturn(ctx, error);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
||||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (state != FiberStateIdle)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations.TryRemove(fiber, out _);
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
_stackRanges.TryRemove(fiber, out _);
|
||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
||||
return ctx.SetReturn(0);
|
||||
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -229,20 +229,20 @@ public static class FiberExports
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
var returnArgument = ctx[CpuRegister.Rdi];
|
||||
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
||||
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
|
||||
if (argOnRunAddress != 0 && !TryWriteUInt64(ctx, argOnRunAddress, 0))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
@@ -256,7 +256,7 @@ public static class FiberExports
|
||||
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
previousFiber = returnTarget.PreviousFiber;
|
||||
@@ -264,10 +264,10 @@ public static class FiberExports
|
||||
{
|
||||
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
||||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
||||
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = previousContinuation.Context with { Rax = 0 };
|
||||
@@ -278,15 +278,15 @@ public static class FiberExports
|
||||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ public static class FiberExports
|
||||
TraceFiber(
|
||||
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
||||
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -309,18 +309,18 @@ public static class FiberExports
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
return TryWriteUInt64(ctx, outAddress, fiberAddress)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -334,35 +334,35 @@ public static class FiberExports
|
||||
var info = ctx[CpuRegister.Rsi];
|
||||
if (info == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
return SetReturn(ctx, error);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
|
||||
if (!TryReadUInt64(ctx, info, out var size) || size != FiberInfoSize)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
||||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
|
||||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
|
||||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
|
||||
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
|
||||
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
|
||||
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
|
||||
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
|
||||
!TryWriteName(ctx, info + 40, fields.Name) ||
|
||||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
||||
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -376,22 +376,22 @@ public static class FiberExports
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
return SetReturn(ctx, error);
|
||||
}
|
||||
|
||||
if (nameAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -403,12 +403,12 @@ public static class FiberExports
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] != 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -419,8 +419,8 @@ public static class FiberExports
|
||||
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
||||
{
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -433,17 +433,17 @@ public static class FiberExports
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
if (ResolveCurrentFiberAddress(ctx) == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
return TryWriteUInt64(ctx, outAddress, ctx[CpuRegister.Rbp])
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
private static int FiberInitializeCore(
|
||||
@@ -459,37 +459,37 @@ public static class FiberExports
|
||||
{
|
||||
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((fiber & 7) != 0 ||
|
||||
(contextAddress & 15) != 0 ||
|
||||
(optParam & 7) != 0)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
return SetReturn(ctx, FiberErrorAlignment);
|
||||
}
|
||||
|
||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorRange);
|
||||
return SetReturn(ctx, FiberErrorRange);
|
||||
}
|
||||
|
||||
if ((contextSize & 15) != 0 ||
|
||||
(contextAddress == 0 && contextSize != 0) ||
|
||||
(contextAddress != 0 && contextSize == 0))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (optParam != 0 &&
|
||||
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
||||
@@ -497,27 +497,27 @@ public static class FiberExports
|
||||
flags |= FiberFlagContextSizeCheck;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
||||
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (contextAddress != 0)
|
||||
{
|
||||
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||
if (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if ((flags & FiberFlagContextSizeCheck) != 0)
|
||||
@@ -532,7 +532,7 @@ public static class FiberExports
|
||||
}
|
||||
|
||||
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int FiberRunCore(
|
||||
@@ -547,12 +547,12 @@ public static class FiberExports
|
||||
{
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return ctx.SetReturn(error);
|
||||
return SetReturn(ctx, error);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (attachContextAddress != 0 || attachContextSize != 0)
|
||||
@@ -560,7 +560,7 @@ public static class FiberExports
|
||||
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
||||
if (attachResult != 0)
|
||||
{
|
||||
return ctx.SetReturn(attachResult);
|
||||
return SetReturn(ctx, attachResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,16 +568,16 @@ public static class FiberExports
|
||||
if ((isSwitch && previousFiber == 0) ||
|
||||
(!isSwitch && previousFiber != 0))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
if (previousFiber == fiber)
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
@@ -586,12 +586,12 @@ public static class FiberExports
|
||||
{
|
||||
if (!TryReadFiberFields(ctx, fiber, out fields))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
if (fields.State != FiberStateIdle)
|
||||
{
|
||||
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
FiberContinuation targetContinuation;
|
||||
@@ -602,12 +602,12 @@ public static class FiberExports
|
||||
}
|
||||
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
var callerContinuation = new FiberContinuation(
|
||||
@@ -616,11 +616,11 @@ public static class FiberExports
|
||||
|
||||
if (previousFiber != 0)
|
||||
{
|
||||
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
|
||||
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
|
||||
previousState != FiberStateRun ||
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
||||
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations[previousFiber] = callerContinuation;
|
||||
@@ -631,15 +631,15 @@ public static class FiberExports
|
||||
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
|
||||
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
|
||||
{
|
||||
if (previousFiber != 0)
|
||||
{
|
||||
_continuations.TryRemove(previousFiber, out _);
|
||||
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
||||
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
|
||||
}
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed)
|
||||
@@ -656,7 +656,7 @@ public static class FiberExports
|
||||
TraceFiber(
|
||||
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
||||
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static bool TryCreateInitialContinuation(
|
||||
@@ -673,7 +673,7 @@ public static class FiberExports
|
||||
|
||||
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
||||
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
||||
if (!ctx.TryWriteUInt64(entryRsp, 0))
|
||||
if (!TryWriteUInt64(ctx, entryRsp, 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -708,7 +708,7 @@ public static class FiberExports
|
||||
FiberContinuation continuation,
|
||||
ulong argument) =>
|
||||
continuation.ArgOnRunAddress == 0 ||
|
||||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
|
||||
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
|
||||
|
||||
private static GuestCpuContinuation CaptureContinuation(
|
||||
CpuContext ctx,
|
||||
@@ -779,11 +779,11 @@ public static class FiberExports
|
||||
return FiberErrorInvalid;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||
{
|
||||
return FiberErrorInvalid;
|
||||
}
|
||||
@@ -839,8 +839,8 @@ public static class FiberExports
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||
magicStart != FiberSignature0 ||
|
||||
magicEnd != FiberSignature1)
|
||||
{
|
||||
@@ -855,12 +855,12 @@ public static class FiberExports
|
||||
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
||||
{
|
||||
fields = default;
|
||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
|
||||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
|
||||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
||||
{
|
||||
return false;
|
||||
@@ -898,6 +898,46 @@ public static class FiberExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
||||
@@ -925,6 +965,37 @@ public static class FiberExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||
{
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||
{
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes[index] = current[0];
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceFiber(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -1,46 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Ime;
|
||||
|
||||
public static class ImeExports
|
||||
{
|
||||
// Quake (KEX) calls this from its main loop and from the audio bring-up path with
|
||||
// an event-handler pointer. No IME session ever exists here, so report success
|
||||
// without invoking the handler ("no pending IME events"). This NID was previously
|
||||
// misbound as an sceNgs2VoiceControl alias, which fed the game NGS2 errors.
|
||||
[SysAbiExport(
|
||||
Nid = "-4GCfYdNF1s",
|
||||
ExportName = "sceImeUpdate",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceIme")]
|
||||
public static int ImeUpdate(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "eaFXjfJv3xs",
|
||||
ExportName = "sceImeKeyboardOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceIme")]
|
||||
public static int ImeKeyboardOpen(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "dKadqZFgKKQ",
|
||||
ExportName = "sceImeKeyboardGetResourceId",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceIme")]
|
||||
public static int ImeKeyboardGetResourceId(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Ampr;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
@@ -49,7 +50,7 @@ public static class KernelAprCompatExports
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
||||
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -140,7 +141,7 @@ public static class KernelAprCompatExports
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
||||
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -149,19 +150,6 @@ public static class KernelAprCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Success stub: the argument layout is unknown and callers tolerate the
|
||||
// empty answer (Quake streams fine), so no output payload is written until
|
||||
// the real signature is reversed.
|
||||
[SysAbiExport(
|
||||
Nid = "WvEu7yl3Ivg",
|
||||
ExportName = "sceKernelAprGetFileSize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelAprGetFileSize(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress)
|
||||
{
|
||||
Span<byte> result = stackalloc byte[sizeof(ulong)];
|
||||
@@ -190,6 +178,13 @@ public static class KernelAprCompatExports
|
||||
return tag is 0x0C or 0x10;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static void TraceApr(
|
||||
CpuContext ctx,
|
||||
string operation,
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
@@ -51,17 +52,17 @@ public static class KernelEventFlagCompatExports
|
||||
optionAddress != 0 ||
|
||||
!IsValidAttributes(attributes))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
||||
@@ -75,11 +76,11 @@ public static class KernelEventFlagCompatExports
|
||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||
{
|
||||
_eventFlags.TryRemove(handle, out _);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceEventFlag($"create handle=0x{handle:X16} name='{name}' attr=0x{attributes:X2} bits=0x{initialPattern:X16}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -92,7 +93,7 @@ public static class KernelEventFlagCompatExports
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
if (!_eventFlags.TryRemove(handle, out var state))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
@@ -101,7 +102,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -116,7 +117,7 @@ public static class KernelEventFlagCompatExports
|
||||
var returnRip = GetCurrentReturnRip();
|
||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
@@ -127,7 +128,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -141,7 +142,7 @@ public static class KernelEventFlagCompatExports
|
||||
var pattern = ctx[CpuRegister.Rsi];
|
||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
@@ -150,7 +151,7 @@ public static class KernelEventFlagCompatExports
|
||||
TraceEventFlag($"clear handle=0x{handle:X16} mask=0x{pattern:X16} bits=0x{state.Bits:X16}");
|
||||
}
|
||||
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -167,29 +168,29 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
}
|
||||
|
||||
ApplyClearMode(state, pattern, waitMode);
|
||||
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,18 +210,18 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
uint timeoutUsec = 0;
|
||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
||||
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Monitor.Enter(state.Gate);
|
||||
@@ -228,15 +229,15 @@ public static class KernelEventFlagCompatExports
|
||||
{
|
||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
||||
{
|
||||
return ctx.SetReturn(immediateWaitResult);
|
||||
return SetReturn(ctx, immediateWaitResult);
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
@@ -271,7 +272,7 @@ public static class KernelEventFlagCompatExports
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (scheduler is null)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
state.WaitingThreads++;
|
||||
@@ -296,7 +297,7 @@ public static class KernelEventFlagCompatExports
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
releaseWaiter = false;
|
||||
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||
return ctx.SetReturn(pumpedWaitResult);
|
||||
return SetReturn(ctx, pumpedWaitResult);
|
||||
}
|
||||
|
||||
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
||||
@@ -313,7 +314,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
state.WaitingThreads++;
|
||||
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -333,15 +334,15 @@ public static class KernelEventFlagCompatExports
|
||||
var waiterCountAddress = ctx[CpuRegister.Rdx];
|
||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
{
|
||||
if (waiterCountAddress != 0 &&
|
||||
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
state.Bits = setPattern;
|
||||
@@ -350,7 +351,7 @@ public static class KernelEventFlagCompatExports
|
||||
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
|
||||
}
|
||||
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static bool IsValidAttributes(uint attributes)
|
||||
@@ -452,6 +453,84 @@ public static class KernelEventFlagCompatExports
|
||||
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
||||
address == 0 || ctx.TryWriteUInt64(address, bits);
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = buffer[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||
{
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||
{
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes[index] = current[0];
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
var value = (int)result;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void TraceEventFlag(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
|
||||
@@ -535,7 +614,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
if (ctx.TryReadByte(address, out var value))
|
||||
if (TryReadByte(ctx, address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X2}");
|
||||
}
|
||||
@@ -543,7 +622,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
if (ctx.TryReadUInt32(address, out var value))
|
||||
if (TryReadUInt32(ctx, address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X8}");
|
||||
}
|
||||
@@ -551,7 +630,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
if (ctx.TryReadUInt64(address, out var value))
|
||||
if (TryReadUInt64(ctx, address, out var value))
|
||||
{
|
||||
builder.Append($" {name}=0x{value:X16}");
|
||||
}
|
||||
|
||||
@@ -287,13 +287,13 @@ public static class KernelEventQueueCompatExports
|
||||
}
|
||||
|
||||
uint timeoutUsec = 0;
|
||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
||||
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -336,7 +336,7 @@ public static class KernelEventQueueCompatExports
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
@@ -541,7 +541,7 @@ public static class KernelEventQueueCompatExports
|
||||
ulong outCountAddress)
|
||||
{
|
||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -654,8 +654,29 @@ public static class KernelEventQueueCompatExports
|
||||
return;
|
||||
}
|
||||
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
|
||||
var returnRip = 0UL;
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -8,13 +8,12 @@ namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelExports
|
||||
{
|
||||
private static int _nextFileDescriptor = 2;
|
||||
private static readonly object _cxaGate = new();
|
||||
private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
|
||||
private static readonly object _coredumpGate = new();
|
||||
private static ulong _coredumpHandler;
|
||||
private static ulong _coredumpHandlerContext;
|
||||
private const uint Gen4CompiledSdkVersion = 0x05000000;
|
||||
private const uint Gen5CompiledSdkVersion = 0x09000000;
|
||||
|
||||
private readonly record struct CxaDestructorEntry(
|
||||
ulong Function,
|
||||
@@ -28,24 +27,7 @@ public static class KernelExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
||||
{
|
||||
var versionAddress = ctx[CpuRegister.Rdi];
|
||||
if (versionAddress == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var sdkVersion = ctx.TargetGeneration == Generation.Gen5
|
||||
? Gen5CompiledSdkVersion
|
||||
: Gen4CompiledSdkVersion;
|
||||
|
||||
if (!ctx.TryWriteUInt32(versionAddress, sdkVersion))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
_ = ctx;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -71,20 +53,12 @@ public static class KernelExports
|
||||
ExportName = "exit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "L1SBTkC+Cvw",
|
||||
ExportName = "abort",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Abort(CpuContext ctx)
|
||||
public static int Exit(CpuContext ctx)
|
||||
{
|
||||
// Route through the same graceful guest-entry-exit path as exit(): letting the call
|
||||
// fall through to the host's native abort() does not unwind the guest thread cleanly.
|
||||
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
|
||||
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
|
||||
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -199,7 +173,11 @@ public static class KernelExports
|
||||
ExportName = "_exit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
|
||||
public static int UnderscoreExit(CpuContext ctx)
|
||||
{
|
||||
_ = ctx;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Ac86z8q7T8A",
|
||||
@@ -278,14 +256,20 @@ public static class KernelExports
|
||||
ExportName = "pthread_create",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(ctx);
|
||||
public static int PosixPthreadCreate(CpuContext ctx)
|
||||
{
|
||||
return PthreadCreate(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Jmi+9w9u0E4",
|
||||
ExportName = "pthread_create_name_np",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
|
||||
public static int PosixPthreadCreateNameNp(CpuContext ctx)
|
||||
{
|
||||
return PthreadCreate(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "3kg7rT0NQIs",
|
||||
@@ -362,7 +346,10 @@ public static class KernelExports
|
||||
ExportName = "pthread_join",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
|
||||
public static int PosixPthreadJoin(CpuContext ctx)
|
||||
{
|
||||
return PthreadJoin(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "wuCroIGjt2g",
|
||||
@@ -451,76 +438,4 @@ public static class KernelExports
|
||||
{
|
||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
private static int RequestProcessExit(CpuContext ctx, string syscallName)
|
||||
{
|
||||
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
|
||||
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Unidentified kernel NIDs observed at runtime; stubbed to return 0 until they are
|
||||
// resolved against scripts/ps5_names.txt.
|
||||
[SysAbiExport(
|
||||
Nid = "9T2pDF2Ryqg",
|
||||
ExportName = "sceKernelUnknown9T2p",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknown9T2p(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "TU-d9PfIHPM",
|
||||
ExportName = "sceKernelUnknownTU",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownTU(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "iWQWrwiSt8A",
|
||||
ExportName = "sceKernelUnknowniWQW",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknowniWQW(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KuOmgKoqCdY",
|
||||
ExportName = "sceKernelUnknownKuOmg",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownKuOmg(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RenI1lL1WFk",
|
||||
ExportName = "sceKernelUnknownRenI",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownRenI(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "pQGpHYopAIY",
|
||||
ExportName = "sceKernelUnknownpQGp",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownpQGp(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Rbvt+5Y2iEw",
|
||||
ExportName = "sceKernelUnknownRbvt",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelUnknownRbvt(CpuContext ctx) => SetZeroReturn(ctx);
|
||||
|
||||
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
|
||||
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
|
||||
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
|
||||
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
|
||||
// shadowing them breaks every C++ static-init guard in the game.
|
||||
|
||||
private static int SetZeroReturn(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,7 +7,9 @@ using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Linq;
|
||||
using System.Globalization;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -53,8 +55,6 @@ public static class KernelMemoryCompatExports
|
||||
private const int OrbisVirtualQueryInfoSize = 72;
|
||||
private const int OrbisKernelMaximumNameLength = 32;
|
||||
private const uint MemCommit = 0x1000;
|
||||
private const uint MemReserve = 0x2000;
|
||||
private const uint MemRelease = 0x8000;
|
||||
private const uint HostPageNoAccess = 0x01;
|
||||
private const uint HostPageReadOnly = 0x02;
|
||||
private const uint HostPageReadWrite = 0x04;
|
||||
@@ -123,7 +123,6 @@ public static class KernelMemoryCompatExports
|
||||
private static int _hostMemoryWriteFallbackCount;
|
||||
private static int _hostMemoryReadFallbackCount;
|
||||
private static int _nullWcscpyRecoveryCount;
|
||||
private static int _nullStrcasecmpRecoveryCount;
|
||||
private static string? _cachedApp0Root;
|
||||
private static string? _cachedDownload0Root;
|
||||
|
||||
@@ -146,13 +145,6 @@ public static class KernelMemoryCompatExports
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
|
||||
|
||||
private sealed class OpenDirectory
|
||||
{
|
||||
public required string Path { get; init; }
|
||||
@@ -161,7 +153,7 @@ public static class KernelMemoryCompatExports
|
||||
}
|
||||
|
||||
private readonly record struct DirectAllocation(ulong Start, ulong Length, int MemoryType);
|
||||
private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment, bool IsGuarded);
|
||||
private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment);
|
||||
private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, bool IsDirect, ulong DirectStart);
|
||||
private readonly record struct BatchMapEntry(ulong Start, ulong Offset, ulong Length, byte Protection, byte Type, int Operation);
|
||||
|
||||
@@ -196,14 +188,6 @@ public static class KernelMemoryCompatExports
|
||||
ulong length,
|
||||
ulong alignment,
|
||||
out ulong address)
|
||||
=> TryAllocateHleData(ctx, length, alignment, OrbisProtCpuReadWrite, out address);
|
||||
|
||||
internal static bool TryAllocateHleData(
|
||||
CpuContext ctx,
|
||||
ulong length,
|
||||
ulong alignment,
|
||||
int protection,
|
||||
out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
if (length == 0 || length > int.MaxValue)
|
||||
@@ -218,7 +202,7 @@ public static class KernelMemoryCompatExports
|
||||
var desiredAddress = AlignUp(
|
||||
_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress,
|
||||
effectiveAlignment);
|
||||
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, protection, effectiveAlignment, out address) ||
|
||||
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, OrbisProtCpuReadWrite, effectiveAlignment, out address) ||
|
||||
address == 0)
|
||||
{
|
||||
return false;
|
||||
@@ -228,7 +212,7 @@ public static class KernelMemoryCompatExports
|
||||
_mappedRegions[address] = new MappedRegion(
|
||||
address,
|
||||
mappedLength,
|
||||
protection,
|
||||
OrbisProtCpuReadWrite,
|
||||
IsFlexible: false,
|
||||
IsDirect: false,
|
||||
DirectStart: 0);
|
||||
@@ -249,69 +233,6 @@ public static class KernelMemoryCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong _dummyVtableAddress;
|
||||
private const int DummyVtableSlotCount = 64;
|
||||
|
||||
// Some KexEngine objects (mutex wrappers, NGS2 system/rack/voice handles, etc.) are treated
|
||||
// by guest code as C++ instances with a vtable pointer at offset 0. When an HLE constructor
|
||||
// leaves that slot zeroed, the game's own virtual dispatch (call [ [obj]+N ]) reads a null
|
||||
// vtable and jumps through address N, crashing (e.g. "CALL qword ptr [RAX+0x58]" with RAX=0).
|
||||
// This allocates one shared, executable no-op stub plus a vtable of pointers to it, so any
|
||||
// HLE object constructor can make virtual calls on its output land safely.
|
||||
internal static bool TryWriteDummyVtable(CpuContext ctx, ulong objectAddress)
|
||||
{
|
||||
if (objectAddress == 0 || !TryEnsureDummyVtable(ctx, out var vtableAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(objectAddress, vtableAddress);
|
||||
}
|
||||
|
||||
private static bool TryEnsureDummyVtable(CpuContext ctx, out ulong vtableAddress)
|
||||
{
|
||||
lock (_memoryGate)
|
||||
{
|
||||
if (_dummyVtableAddress != 0)
|
||||
{
|
||||
vtableAddress = _dummyVtableAddress;
|
||||
return true;
|
||||
}
|
||||
|
||||
const int executableReadWrite = OrbisProtCpuRead | OrbisProtCpuWrite | OrbisProtCpuExec;
|
||||
if (!TryAllocateHleData(ctx, 0x1000, 0x1000, executableReadWrite, out var block))
|
||||
{
|
||||
vtableAddress = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
// xor eax, eax; ret - every dummy virtual method just returns 0 and does nothing else.
|
||||
if (!ctx.Memory.TryWrite(block, new byte[] { 0x31, 0xC0, 0xC3 }))
|
||||
{
|
||||
vtableAddress = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
var table = new byte[DummyVtableSlotCount * sizeof(ulong)];
|
||||
for (var i = 0; i < DummyVtableSlotCount; i++)
|
||||
{
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(table.AsSpan(i * sizeof(ulong), sizeof(ulong)), block);
|
||||
}
|
||||
|
||||
var tableAddress = block + 0x100;
|
||||
if (!ctx.Memory.TryWrite(tableAddress, table))
|
||||
{
|
||||
vtableAddress = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Dummy vtable ready: stub=0x{block:X16} vtable=0x{tableAddress:X16} slots={DummyVtableSlotCount}");
|
||||
_dummyVtableAddress = tableAddress;
|
||||
vtableAddress = tableAddress;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void RegisterReservedVirtualRange(ulong address, ulong length)
|
||||
{
|
||||
if (address == 0 || length == 0)
|
||||
@@ -362,18 +283,6 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Longer null-dst memsets are unrecoverable, but RAX must still be set - leaving it
|
||||
// stale here previously let callers that do `buf = memset(...)` carry on with a
|
||||
// garbage "buffer" pointer instead of a clean NULL, causing a *different*,
|
||||
// confusingly-located crash further downstream.
|
||||
var largeRecoveryIndex = Interlocked.Increment(ref _nullMemsetRecoveryCount);
|
||||
if (largeRecoveryIndex <= 8)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARNING] memset null-dst (len>0x20) recovery#{largeRecoveryIndex}: rip=0x{ctx.Rip:X16} len=0x{length:X} val=0x{value:X2}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
@@ -397,7 +306,6 @@ public static class KernelMemoryCompatExports
|
||||
Console.WriteLine("!!! CRITICAL: Bad Memset Call !!!");
|
||||
Console.WriteLine($"Called from RIP: 0x{ctx.Rip:X}");
|
||||
Console.WriteLine($"dst=0x{destination:X} val=0x{value:X2} len=0x{length:X}");
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
@@ -630,80 +538,6 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AV6ipCNa4Rw",
|
||||
ExportName = "strcasecmp",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Strcasecmp(CpuContext ctx)
|
||||
{
|
||||
var left = ctx[CpuRegister.Rdi];
|
||||
var right = ctx[CpuRegister.Rsi];
|
||||
if (left == 0 || right == 0)
|
||||
{
|
||||
var recoveryIndex = Interlocked.Increment(ref _nullStrcasecmpRecoveryCount);
|
||||
if (recoveryIndex <= 16)
|
||||
{
|
||||
var otherAddress = left == 0 ? right : left;
|
||||
var otherText = otherAddress != 0 && TryReadNullTerminatedUtf8(ctx, otherAddress, 256, out var text)
|
||||
? text
|
||||
: "<unreadable>";
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARNING] strcasecmp null-arg recovery#{recoveryIndex}: ret=0x{returnRip:X16} left=0x{left:X16} right=0x{right:X16} other=\"{otherText}\"");
|
||||
}
|
||||
|
||||
// Real strcasecmp(NULL, x) is undefined behaviour and previously crashed inside the
|
||||
// LLE-routed implementation. Treat it as "not equal" instead so callers doing
|
||||
// `if (strcasecmp(a, b) == 0)` degrade gracefully rather than taking down the guest.
|
||||
ctx[CpuRegister.Rax] = left == right ? 0uL : 1uL;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!TryCompareStringsCaseInsensitive(ctx, left, right, limit: ulong.MaxValue, out var compare))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 1;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)compare);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryCompareStringsCaseInsensitive(CpuContext ctx, ulong left, ulong right, ulong limit, out int compare)
|
||||
{
|
||||
compare = 0;
|
||||
if (left == 0 || right == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var max = limit == ulong.MaxValue ? 1_048_576UL : Math.Min(limit, 1_048_576UL);
|
||||
Span<byte> leftByte = stackalloc byte[1];
|
||||
Span<byte> rightByte = stackalloc byte[1];
|
||||
for (ulong i = 0; i < max; i++)
|
||||
{
|
||||
if (!TryReadCompat(ctx, left + i, leftByte) ||
|
||||
!TryReadCompat(ctx, right + i, rightByte))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var leftLower = ToAsciiLower(leftByte[0]);
|
||||
var rightLower = ToAsciiLower(rightByte[0]);
|
||||
compare = leftLower - rightLower;
|
||||
if (compare != 0 || leftByte[0] == 0 || rightByte[0] == 0)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
compare = 0;
|
||||
return true;
|
||||
}
|
||||
|
||||
private static byte ToAsciiLower(byte value) => value is >= (byte)'A' and <= (byte)'Z' ? (byte)(value + 32) : value;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "0nV21JjYCH8",
|
||||
ExportName = "wcsncpy",
|
||||
@@ -755,54 +589,6 @@ public static class KernelMemoryCompatExports
|
||||
return VsnprintfCore(ctx);
|
||||
}
|
||||
|
||||
// sprintf/vsprintf are served from the same HLE formatting engine as snprintf/vsnprintf
|
||||
// instead of falling through to the game's bundled libc.
|
||||
[SysAbiExport(
|
||||
Nid = "tcVi5SivF7Q",
|
||||
ExportName = "sprintf",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Sprintf(CpuContext ctx)
|
||||
{
|
||||
var destination = ctx[CpuRegister.Rdi];
|
||||
var formatAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var format = Encoding.UTF8.GetString(formatBytes);
|
||||
var rendered = FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
|
||||
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, rendered);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "jbz9I9vkqkk",
|
||||
ExportName = "vsprintf",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Vsprintf(CpuContext ctx)
|
||||
{
|
||||
var destination = ctx[CpuRegister.Rdi];
|
||||
var formatAddress = ctx[CpuRegister.Rsi];
|
||||
var vaListAddress = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var format = Encoding.UTF8.GetString(formatBytes);
|
||||
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
|
||||
{
|
||||
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, formatBytes);
|
||||
}
|
||||
|
||||
var rendered = FormatString(ctx, format, ref vaCursor);
|
||||
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, rendered);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "nJz16JE1txM",
|
||||
ExportName = "swprintf",
|
||||
@@ -1161,27 +947,15 @@ public static class KernelMemoryCompatExports
|
||||
{
|
||||
var destination = ctx[CpuRegister.Rdi];
|
||||
var source = ctx[CpuRegister.Rsi];
|
||||
var rawCount = ctx[CpuRegister.Rdx];
|
||||
|
||||
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
|
||||
// upstream bug that also feeds bad lengths to memset) must not reach
|
||||
// GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread
|
||||
// call context corrupted the CLR outright ("Invalid Program: attempted to call a
|
||||
// UnmanagedCallersOnly method from managed code") instead of throwing a normal
|
||||
// exception. Reject anything above a sane bound before allocating.
|
||||
const ulong maxSaneCount = 512UL * 1024 * 1024;
|
||||
if (rawCount > maxSaneCount)
|
||||
var count = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
||||
if (count < 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARNING] memcpy oversized count rejected: dst=0x{destination:X16} src=0x{source:X16} count=0x{rawCount:X}");
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var count = (int)rawCount;
|
||||
var payload = GC.AllocateUninitializedArray<byte>(count);
|
||||
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
@@ -1424,24 +1198,6 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Was unresolved (returning the 0x80020002 sentinel, then crashing when the guest
|
||||
// dereferenced it) - the game's own heap instrumentation calls this hook when it
|
||||
// detects a corrupted/invalid block, not the emulator's allocator, so this is purely
|
||||
// a diagnostic sink: log what was reported and return success so the caller continues.
|
||||
[SysAbiExport(
|
||||
Nid = "al3JzFI9MQ0",
|
||||
ExportName = "sceLibcInternalHeapErrorReportForGame",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceLibcInternal")]
|
||||
public static int LibcInternalHeapErrorReportForGame(CpuContext ctx)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] sceLibcInternalHeapErrorReportForGame: rdi=0x{ctx[CpuRegister.Rdi]:X16} " +
|
||||
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16}");
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "DfivPArhucg",
|
||||
ExportName = "memcmp",
|
||||
@@ -2239,37 +1995,23 @@ public static class KernelMemoryCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int ClockGettime(CpuContext ctx)
|
||||
{
|
||||
var clockId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
var timespecAddress = ctx[CpuRegister.Rsi];
|
||||
if (timespecAddress == 0)
|
||||
{
|
||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
||||
return -1;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
long seconds;
|
||||
long nanoseconds;
|
||||
if (!KernelRuntimeCompatExports.ResolveClockTime(clockId, out seconds, out nanoseconds))
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var seconds = now.ToUnixTimeSeconds();
|
||||
var nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
||||
if (!ctx.TryWriteUInt64(timespecAddress, unchecked((ulong)seconds)) ||
|
||||
!ctx.TryWriteUInt64(timespecAddress + sizeof(long), unchecked((ulong)nanoseconds)))
|
||||
{
|
||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
||||
return -1;
|
||||
}
|
||||
|
||||
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
|
||||
|
||||
if (!ctx.Memory.TryWrite(timespecAddress, timespecBuffer))
|
||||
{
|
||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
||||
return -1;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -2789,17 +2531,6 @@ public static class KernelMemoryCompatExports
|
||||
if (fixedMapping && requestedAddress != 0)
|
||||
{
|
||||
mappedAddress = requestedAddress;
|
||||
reserved = IsGuestRangeBacked(ctx, requestedAddress, length);
|
||||
if (!reserved)
|
||||
{
|
||||
TryReserveExactGuestVirtualRange(ctx, requestedAddress, length, protection);
|
||||
reserved = IsGuestRangeBacked(ctx, requestedAddress, length);
|
||||
}
|
||||
|
||||
if (!reserved)
|
||||
{
|
||||
mappedAddress = 0;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
@@ -2810,7 +2541,7 @@ public static class KernelMemoryCompatExports
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] map_direct reserve: requested=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} reserved={reserved} mapped=0x{mappedAddress:X16}");
|
||||
}
|
||||
if (!reserved && !fixedMapping)
|
||||
if (!reserved)
|
||||
{
|
||||
if (mappedAddress == 0)
|
||||
{
|
||||
@@ -2894,18 +2625,12 @@ public static class KernelMemoryCompatExports
|
||||
if (fixedMapping && requestedAddress != 0)
|
||||
{
|
||||
mappedAddress = requestedAddress;
|
||||
if (!IsGuestRangeBacked(ctx, requestedAddress, length))
|
||||
{
|
||||
TryReserveExactGuestVirtualRange(ctx, requestedAddress, length, protection);
|
||||
if (!IsGuestRangeBacked(ctx, requestedAddress, length))
|
||||
{
|
||||
mappedAddress = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
else if (!TryReserveGuestVirtualRange(ctx, desiredAddress, length, protection, OrbisPageSize, out mappedAddress))
|
||||
{
|
||||
mappedAddress = AllocateMappedGuestAddress(ctx, length, 0x1000UL);
|
||||
mappedAddress = requestedAddress != 0 && fixedMapping
|
||||
? requestedAddress
|
||||
: AllocateMappedGuestAddress(ctx, length, 0x1000UL);
|
||||
}
|
||||
|
||||
if (ShouldTraceDirectMemory())
|
||||
@@ -3163,7 +2888,7 @@ public static class KernelMemoryCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (protectionOut != 0 && !ctx.TryWriteInt32(protectionOut, region.Protection))
|
||||
if (protectionOut != 0 && !TryWriteInt32(ctx, protectionOut, region.Protection))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -3202,7 +2927,7 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
if (!ctx.TryWriteUInt64(infoAddress, block.Start) ||
|
||||
!ctx.TryWriteUInt64(infoAddress + sizeof(ulong), block.Start + block.Length) ||
|
||||
!ctx.TryWriteInt32(infoAddress + (sizeof(ulong) * 2), block.MemoryType))
|
||||
!TryWriteInt32(ctx, infoAddress + (sizeof(ulong) * 2), block.MemoryType))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -4060,7 +3785,7 @@ public static class KernelMemoryCompatExports
|
||||
var addr = argumentSource.NextGpArg();
|
||||
if (addr != 0)
|
||||
{
|
||||
_ = ctx.TryWriteInt32(addr, sb.Length);
|
||||
_ = TryWriteInt32(ctx, addr, sb.Length);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -4106,59 +3831,33 @@ public static class KernelMemoryCompatExports
|
||||
|
||||
private struct RegisterPrintfArgumentSource : IPrintfArgumentSource
|
||||
{
|
||||
// SysV AMD64: variadic integer/pointer args use the GP registers (rdi..r9, up to
|
||||
// 6) and variadic float/double args use xmm0..xmm7 (8) — INDEPENDENT counters.
|
||||
// Args beyond the registers spill to the stack in source order, so GP-overflow
|
||||
// and FP-overflow share one stack cursor.
|
||||
private readonly CpuContext _ctx;
|
||||
private int _gpIndex;
|
||||
private int _fpIndex;
|
||||
private int _stackIndex;
|
||||
|
||||
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
|
||||
{
|
||||
_ctx = ctx;
|
||||
_gpIndex = gpIndex;
|
||||
_fpIndex = 0;
|
||||
_stackIndex = 0;
|
||||
}
|
||||
|
||||
public ulong NextGpArg()
|
||||
{
|
||||
var index = _gpIndex;
|
||||
if (index < 6)
|
||||
var index = _gpIndex++;
|
||||
return index switch
|
||||
{
|
||||
_gpIndex++;
|
||||
return index switch
|
||||
{
|
||||
0 => _ctx[CpuRegister.Rdi],
|
||||
1 => _ctx[CpuRegister.Rsi],
|
||||
2 => _ctx[CpuRegister.Rdx],
|
||||
3 => _ctx[CpuRegister.Rcx],
|
||||
4 => _ctx[CpuRegister.R8],
|
||||
_ => _ctx[CpuRegister.R9],
|
||||
};
|
||||
}
|
||||
|
||||
return ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
|
||||
0 => _ctx[CpuRegister.Rdi],
|
||||
1 => _ctx[CpuRegister.Rsi],
|
||||
2 => _ctx[CpuRegister.Rdx],
|
||||
3 => _ctx[CpuRegister.Rcx],
|
||||
4 => _ctx[CpuRegister.R8],
|
||||
5 => _ctx[CpuRegister.R9],
|
||||
_ => ReadStackArg(_ctx, (ulong)(index - 6) * 8)
|
||||
};
|
||||
}
|
||||
|
||||
public double NextFloatArg()
|
||||
{
|
||||
// Float/double variadic args live in xmm0..xmm7 (low 64 bits), NOT the GP
|
||||
// registers. Reading them from GP prints garbage and desynchronizes every
|
||||
// subsequent argument.
|
||||
ulong bits;
|
||||
if (_fpIndex < 8)
|
||||
{
|
||||
_ctx.GetXmmRegister(_fpIndex++, out bits, out _);
|
||||
}
|
||||
else
|
||||
{
|
||||
bits = ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
|
||||
}
|
||||
|
||||
return BitConverter.Int64BitsToDouble(unchecked((long)bits));
|
||||
return BitConverter.Int64BitsToDouble(unchecked((long)NextGpArg()));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4296,37 +3995,6 @@ public static class KernelMemoryCompatExports
|
||||
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;
|
||||
}
|
||||
|
||||
Span<byte> probe = stackalloc byte[1];
|
||||
return ctx.Memory.TryRead(address, probe) &&
|
||||
ctx.Memory.TryRead(address + length - 1, probe);
|
||||
}
|
||||
|
||||
private static bool IsMappedGuestRangeAvailable(
|
||||
CpuContext ctx,
|
||||
ulong address,
|
||||
@@ -4397,7 +4065,7 @@ public static class KernelMemoryCompatExports
|
||||
return FileMode.Open;
|
||||
}
|
||||
|
||||
public static string ResolveGuestPath(string guestPath)
|
||||
private static string ResolveGuestPath(string guestPath)
|
||||
{
|
||||
if (string.IsNullOrWhiteSpace(guestPath))
|
||||
{
|
||||
@@ -4721,7 +4389,7 @@ public static class KernelMemoryCompatExports
|
||||
private static bool IsMutatingOpen(int flags) =>
|
||||
(flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0;
|
||||
|
||||
public static bool IsReadOnlyGuestMutationPath(string guestPath)
|
||||
private static bool IsReadOnlyGuestMutationPath(string guestPath)
|
||||
{
|
||||
var normalized = NormalizeGuestStatCachePath(guestPath);
|
||||
return normalized is not null &&
|
||||
@@ -4811,7 +4479,7 @@ public static class KernelMemoryCompatExports
|
||||
continue;
|
||||
}
|
||||
|
||||
var one = chunk.AsSpan(0, 1);
|
||||
Span<byte> one = stackalloc byte[1];
|
||||
if (!TryReadCompat(ctx, current, one))
|
||||
{
|
||||
return false;
|
||||
@@ -5000,7 +4668,7 @@ public static class KernelMemoryCompatExports
|
||||
return destination == 0 || destinationCount == 0 || TryWriteWideTerminator(ctx, destination);
|
||||
}
|
||||
|
||||
public static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (address == 0 || maxLength <= 0)
|
||||
@@ -5250,7 +4918,7 @@ public static class KernelMemoryCompatExports
|
||||
processedCount++;
|
||||
}
|
||||
|
||||
if (processedOutAddress != 0 && !ctx.TryWriteInt32(processedOutAddress, processedCount))
|
||||
if (processedOutAddress != 0 && !TryWriteInt32(ctx, processedOutAddress, processedCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -5789,26 +5457,6 @@ public static class KernelMemoryCompatExports
|
||||
alignment = NormalizeLibcAlignment(alignment);
|
||||
var actualSize = requestedSize == 0 ? 1u : requestedSize;
|
||||
|
||||
// This intentionally uses host guard pages for allocations made through the libc HLE.
|
||||
// It turns an overrun that reaches the next page into an immediate access violation at
|
||||
// the offending guest instruction, rather than allowing it to poison a later import.
|
||||
var guardHeap = string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_GUARD_HEAP"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
if (guardHeap && TryAllocateGuardedLibcHeap(actualSize, alignment, zeroFill, out address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
// Do not silently fall back to an unguarded allocation when guard heap was explicitly
|
||||
// requested: a failed setup must remain visible to the caller/reproducer.
|
||||
if (guardHeap)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
nuint totalSize;
|
||||
try
|
||||
{
|
||||
@@ -5844,7 +5492,7 @@ public static class KernelMemoryCompatExports
|
||||
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)IntPtr.Size, (ulong)alignment);
|
||||
lock (_libcAllocGate)
|
||||
{
|
||||
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment, IsGuarded: false);
|
||||
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment);
|
||||
}
|
||||
|
||||
try
|
||||
@@ -5864,51 +5512,6 @@ public static class KernelMemoryCompatExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static unsafe bool TryAllocateGuardedLibcHeap(nuint actualSize, nuint alignment, bool zeroFill, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
const nuint pageSize = 0x1000;
|
||||
|
||||
try
|
||||
{
|
||||
var effectiveAlignment = Math.Max(alignment, pageSize);
|
||||
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
|
||||
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
|
||||
var baseAddress = VirtualAlloc(0, reservationSize, MemCommit | MemReserve, HostPageReadWrite);
|
||||
if (baseAddress == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
|
||||
var guardAddress = alignedAddress + (ulong)usableSize;
|
||||
if (!VirtualProtect((nint)guardAddress, pageSize, HostPageNoAccess, out _))
|
||||
{
|
||||
_ = VirtualFree(baseAddress, 0, MemRelease);
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_libcAllocGate)
|
||||
{
|
||||
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment, IsGuarded: true);
|
||||
}
|
||||
|
||||
if (zeroFill)
|
||||
{
|
||||
NativeMemory.Clear((void*)alignedAddress, actualSize);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] guard-heap alloc: ptr=0x{alignedAddress:X16} size=0x{actualSize:X} guard=0x{guardAddress:X16}");
|
||||
address = alignedAddress;
|
||||
return true;
|
||||
}
|
||||
catch (OverflowException)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static unsafe bool TryReallocateLibcHeap(ulong existingAddress, ulong requestedSize, out ulong resizedAddress)
|
||||
{
|
||||
resizedAddress = 0;
|
||||
@@ -6017,14 +5620,7 @@ public static class KernelMemoryCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
if (allocation.IsGuarded)
|
||||
{
|
||||
_ = VirtualFree(allocation.BaseAddress, 0, MemRelease);
|
||||
}
|
||||
else
|
||||
{
|
||||
Marshal.FreeHGlobal(allocation.BaseAddress);
|
||||
}
|
||||
Marshal.FreeHGlobal(allocation.BaseAddress);
|
||||
}
|
||||
|
||||
private static bool TryMultiplyAllocationSize(ulong left, ulong right, out nuint size)
|
||||
@@ -6159,6 +5755,13 @@ public static class KernelMemoryCompatExports
|
||||
return (protect & expected) != 0;
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BitConverter.TryWriteBytes(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static bool TryWriteOpenDescriptorStat(CpuContext ctx, int fd, ulong statAddress)
|
||||
{
|
||||
if (fd is 0 or 1 or 2)
|
||||
@@ -6630,15 +6233,4 @@ public static class KernelMemoryCompatExports
|
||||
sum = left + right;
|
||||
return sum >= left;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KMcEa+rHsIo",
|
||||
ExportName = "sceKernelMapMemory",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelMapMemory(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -327,12 +329,6 @@ public static class KernelPthreadCompatExports
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
||||
[SysAbiExport(
|
||||
Nid = "27bAgiJmOh0",
|
||||
ExportName = "pthread_cond_timedwait",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "m5-2bsNfv7s",
|
||||
@@ -387,42 +383,42 @@ public static class KernelPthreadCompatExports
|
||||
var initRoutine = ctx[CpuRegister.Rsi];
|
||||
if (onceAddress == 0 || initRoutine == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadInt32(onceAddress, out var onceValue))
|
||||
if (!TryReadInt32(ctx, onceAddress, out var onceValue))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (onceValue == PthreadOnceDone)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
var gate = GetPthreadOnceGate(onceAddress);
|
||||
var shouldCall = false;
|
||||
lock (gate)
|
||||
{
|
||||
if (!ctx.TryReadInt32(onceAddress, out onceValue))
|
||||
if (!TryReadInt32(ctx, onceAddress, out onceValue))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
while (onceValue == PthreadOnceInProgress)
|
||||
{
|
||||
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
|
||||
if (!ctx.TryReadInt32(onceAddress, out onceValue))
|
||||
if (!TryReadInt32(ctx, onceAddress, out onceValue))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (onceValue != PthreadOnceDone)
|
||||
{
|
||||
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceInProgress))
|
||||
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceInProgress))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
shouldCall = true;
|
||||
@@ -438,21 +434,21 @@ public static class KernelPthreadCompatExports
|
||||
{
|
||||
lock (gate)
|
||||
{
|
||||
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
||||
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
}
|
||||
|
||||
TracePthreadOnce(onceAddress, initRoutine, "failed", error);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
lock (gate)
|
||||
{
|
||||
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceDone))
|
||||
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceDone))
|
||||
{
|
||||
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
||||
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||
Monitor.PulseAll(gate);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Monitor.PulseAll(gate);
|
||||
@@ -460,7 +456,7 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||
@@ -1022,12 +1018,19 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
|
||||
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
||||
ctx.TryWriteUInt32(address + 0x20, unchecked((uint)state.Type)) &&
|
||||
ctx.TryWriteUInt32(address + 0x3C, unchecked((uint)state.Protocol));
|
||||
TryWriteUInt32(ctx, address + 0x20, unchecked((uint)state.Type)) &&
|
||||
TryWriteUInt32(ctx, address + 0x3C, unchecked((uint)state.Protocol));
|
||||
|
||||
private static bool WriteMutexAttrObject(CpuContext ctx, ulong address, PthreadMutexAttrState state) =>
|
||||
ctx.TryWriteUInt32(address, unchecked((uint)state.Type)) &&
|
||||
ctx.TryWriteUInt32(address + 4, unchecked((uint)state.Protocol));
|
||||
TryWriteUInt32(ctx, address, unchecked((uint)state.Type)) &&
|
||||
TryWriteUInt32(ctx, address + 4, unchecked((uint)state.Protocol));
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BitConverter.TryWriteBytes(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static int PthreadCondInitCore(CpuContext ctx, ulong condAddress)
|
||||
{
|
||||
@@ -1319,6 +1322,32 @@ public static class KernelPthreadCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
|
||||
{
|
||||
var createdState = new PthreadMutexState
|
||||
|
||||
@@ -2,8 +2,10 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using System.Diagnostics.CodeAnalysis;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -20,7 +22,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
private const int DefaultSchedPriority = DefaultThreadPriority;
|
||||
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
|
||||
private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_0000_0000;
|
||||
private const ulong SyntheticRwlockAttrHandleBase = 0x00006005_0000_0000;
|
||||
|
||||
private static readonly object _stateGate = new();
|
||||
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
|
||||
@@ -30,7 +31,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
private static int _nextTlsKey = 1;
|
||||
private static long _nextSyntheticRwlockHandleId = 1;
|
||||
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
||||
private static long _nextSyntheticRwlockAttrHandleId = 1;
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
||||
|
||||
@@ -277,7 +277,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteInt32(outPriorityAddress, priority))
|
||||
if (!TryWriteInt32(ctx, outPriorityAddress, priority))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -324,7 +324,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
|
||||
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -494,7 +494,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteInt32(outStateAddress, state.DetachState))
|
||||
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -737,7 +737,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteInt32(schedParamAddress, state.SchedPriority))
|
||||
if (!TryWriteInt32(ctx, schedParamAddress, state.SchedPriority))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -760,7 +760,7 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadInt32(schedParamAddress, out var schedPriority))
|
||||
if (!TryReadInt32(ctx, schedParamAddress, out var schedPriority))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -839,37 +839,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
return PthreadAttrSetstacksize(ctx);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Bvn74vj6oLo",
|
||||
ExportName = "scePthreadAttrSetstack",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadAttrSetstack(CpuContext ctx)
|
||||
{
|
||||
var attrAddress = ctx[CpuRegister.Rdi];
|
||||
var stackAddress = ctx[CpuRegister.Rsi];
|
||||
var stackSize = ctx[CpuRegister.Rdx];
|
||||
if (attrAddress == 0 || stackAddress == 0 || stackSize == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
|
||||
lock (_stateGate)
|
||||
{
|
||||
var state = GetOrCreateAttrStateLocked(resolvedAddress);
|
||||
var updated = state with { StackAddress = stackAddress, StackSize = stackSize };
|
||||
_attrStates[resolvedAddress] = updated;
|
||||
if (resolvedAddress != attrAddress)
|
||||
{
|
||||
_attrStates[attrAddress] = updated;
|
||||
}
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "6ULAa0fq4jA",
|
||||
ExportName = "scePthreadRwlockInit",
|
||||
@@ -1047,47 +1016,6 @@ public static class KernelPthreadExtendedCompatExports
|
||||
LibraryName = "libKernel")]
|
||||
public static int PosixPthreadRwlockUnlock(CpuContext ctx) => PthreadRwlockUnlock(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "yOfGg-I1ZII",
|
||||
ExportName = "scePthreadRwlockattrInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadRwlockattrInit(CpuContext ctx)
|
||||
{
|
||||
var attrAddress = ctx[CpuRegister.Rdi];
|
||||
if (attrAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var syntheticHandle = AllocateSyntheticHandle(SyntheticRwlockAttrHandleBase, ref _nextSyntheticRwlockAttrHandleId);
|
||||
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, syntheticHandle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "i2ifZ3fS2fo",
|
||||
ExportName = "scePthreadRwlockattrDestroy",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int PthreadRwlockattrDestroy(CpuContext ctx)
|
||||
{
|
||||
var attrAddress = ctx[CpuRegister.Rdi];
|
||||
if (attrAddress == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, 0);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "mqULNdimTn0",
|
||||
ExportName = "pthread_key_create",
|
||||
@@ -1112,12 +1040,12 @@ public static class KernelPthreadExtendedCompatExports
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteInt32(outKeyAddress, key))
|
||||
if (!TryWriteInt32(ctx, outKeyAddress, key))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
ctx[CpuRegister.Rax] = unchecked((uint)key);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
@@ -1449,4 +1377,24 @@ public static class KernelPthreadExtendedCompatExports
|
||||
payload[^1] = 0;
|
||||
return ctx.Memory.TryWrite(address, payload);
|
||||
}
|
||||
|
||||
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!ctx.Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,32 +4,19 @@
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Fiber;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.IO;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Runtime.Intrinsics.X86;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
|
||||
public static class KernelRuntimeCompatExports
|
||||
{
|
||||
internal const int ClockRealtime = 0;
|
||||
internal const int ClockVirtual = 1;
|
||||
internal const int ClockProf = 2;
|
||||
internal const int ClockMonotonic = 4;
|
||||
internal const int ClockUptime = 5;
|
||||
internal const int ClockUptimePrecise = 7;
|
||||
internal const int ClockUptimeFast = 8;
|
||||
internal const int ClockRealtimePrecise = 9;
|
||||
internal const int ClockRealtimeFast = 10;
|
||||
internal const int ClockMonotonicPrecise = 11;
|
||||
internal const int ClockMonotonicFast = 12;
|
||||
internal const int ClockSecond = 13;
|
||||
internal const int ClockThreadCputimeId = 14;
|
||||
internal const int ClockProcTime = 15;
|
||||
private const int Efault = 14;
|
||||
private const int Einval = 22;
|
||||
private const ulong TlsErrnoOffset = 0x40;
|
||||
private const ulong TlsStackChkGuardBaseOffset = 0x800;
|
||||
private const ulong StackChkGuardFieldOffset = 0x10;
|
||||
@@ -80,77 +67,6 @@ public static class KernelRuntimeCompatExports
|
||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||
private delegate ulong RdtscDelegate();
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "QvsZxomvUHs",
|
||||
ExportName = "sceKernelNanosleep",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelNanosleep(CpuContext ctx)
|
||||
{
|
||||
var requestAddress = ctx[CpuRegister.Rdi];
|
||||
var remainAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (requestAddress == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Einval;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Efault;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer);
|
||||
var tvNsec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer[sizeof(long)..]);
|
||||
|
||||
if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = Einval;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (tvSec == 0 && tvNsec == 0)
|
||||
{
|
||||
WriteRemainingTime(ctx, remainAddress, 0, 0);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelNanosleep");
|
||||
|
||||
// TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to
|
||||
// a single tick rather than collapsing to a zero-length (no-op) sleep.
|
||||
var totalTicks = tvSec * TimeSpan.TicksPerSecond + Math.Max(tvNsec / 100L, 1L);
|
||||
try
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromTicks(totalTicks));
|
||||
}
|
||||
catch (ArgumentOutOfRangeException)
|
||||
{
|
||||
Thread.Sleep(TimeSpan.FromMilliseconds(int.MaxValue));
|
||||
}
|
||||
|
||||
WriteRemainingTime(ctx, remainAddress, 0, 0);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds)
|
||||
{
|
||||
if (remainAddress == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Span<byte> remainBuffer = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(remainBuffer, seconds);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(remainBuffer[sizeof(long)..], nanoseconds);
|
||||
ctx.Memory.TryWrite(remainAddress, remainBuffer);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "1jfXLRVzisc",
|
||||
ExportName = "sceKernelUsleep",
|
||||
@@ -273,16 +189,21 @@ public static class KernelRuntimeCompatExports
|
||||
|
||||
long seconds;
|
||||
long nanoseconds;
|
||||
if (!ResolveClockTime(clockId, out seconds, out nanoseconds))
|
||||
if (clockId == 0)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
seconds = now.ToUnixTimeSeconds();
|
||||
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
||||
}
|
||||
else
|
||||
{
|
||||
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
|
||||
seconds = elapsedTicks / Stopwatch.Frequency;
|
||||
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
|
||||
}
|
||||
|
||||
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
|
||||
|
||||
if (!ctx.Memory.TryWrite(timeAddress, timespecBuffer))
|
||||
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
|
||||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)nanoseconds)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -307,11 +228,8 @@ public static class KernelRuntimeCompatExports
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
var seconds = now.ToUnixTimeSeconds();
|
||||
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
|
||||
|
||||
Span<byte> timevalBuffer = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer, seconds);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer[sizeof(long)..], microseconds);
|
||||
if (!ctx.Memory.TryWrite(timeAddress, timevalBuffer))
|
||||
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
|
||||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds)))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -333,28 +251,18 @@ public static class KernelRuntimeCompatExports
|
||||
var seconds = now.ToUnixTimeSeconds();
|
||||
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
|
||||
|
||||
if (timeAddress != 0)
|
||||
if (timeAddress != 0 &&
|
||||
(!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
|
||||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds))))
|
||||
{
|
||||
Span<byte> timevalBuffer = stackalloc byte[16];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer, seconds);
|
||||
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer[sizeof(long)..], microseconds);
|
||||
if (!ctx.Memory.TryWrite(timeAddress, timevalBuffer))
|
||||
{
|
||||
TrySetErrno(ctx, Efault);
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
if (timezoneAddress != 0)
|
||||
if (timezoneAddress != 0 &&
|
||||
(!TryWriteInt32(ctx, timezoneAddress, 0) ||
|
||||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), 0)))
|
||||
{
|
||||
Span<byte> timezoneBuffer = stackalloc byte[8];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(timezoneBuffer, 0);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(timezoneBuffer[sizeof(int)..], 0);
|
||||
if (!ctx.Memory.TryWrite(timezoneAddress, timezoneBuffer))
|
||||
{
|
||||
TrySetErrno(ctx, Efault);
|
||||
return -1;
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
@@ -709,58 +617,7 @@ public static class KernelRuntimeCompatExports
|
||||
internal static bool TrySetErrno(CpuContext ctx, int value)
|
||||
{
|
||||
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
||||
return address != 0 && ctx.TryWriteInt32(address, value);
|
||||
}
|
||||
|
||||
internal static bool ResolveClockTime(int clockId, out long seconds, out long nanoseconds)
|
||||
{
|
||||
switch (clockId)
|
||||
{
|
||||
case ClockRealtime:
|
||||
case ClockRealtimePrecise:
|
||||
case ClockRealtimeFast:
|
||||
case ClockVirtual:
|
||||
case ClockProf:
|
||||
{
|
||||
var now = DateTimeOffset.UtcNow;
|
||||
seconds = now.ToUnixTimeSeconds();
|
||||
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
||||
return true;
|
||||
}
|
||||
|
||||
// CLOCK_SECOND is FreeBSD's cached whole-second realtime clock (Quake's
|
||||
// audio_output_thread polls it and treated the previous EINVAL as a fatal
|
||||
// init failure, exiting and getting respawned in a loop).
|
||||
case ClockSecond:
|
||||
seconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||
nanoseconds = 0;
|
||||
return true;
|
||||
|
||||
case ClockMonotonic:
|
||||
case ClockMonotonicPrecise:
|
||||
case ClockMonotonicFast:
|
||||
case ClockUptime:
|
||||
case ClockUptimePrecise:
|
||||
case ClockUptimeFast:
|
||||
// Per-thread/process CPU time approximated with the monotonic clock; games
|
||||
// use these for profiling deltas where monotonicity matters, not absolutes.
|
||||
case ClockThreadCputimeId:
|
||||
case ClockProcTime:
|
||||
GetProcessMonotonicTime(out seconds, out nanoseconds);
|
||||
return true;
|
||||
|
||||
default:
|
||||
seconds = 0;
|
||||
nanoseconds = 0;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void GetProcessMonotonicTime(out long seconds, out long nanoseconds)
|
||||
{
|
||||
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
|
||||
seconds = elapsedTicks / Stopwatch.Frequency;
|
||||
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
|
||||
return address != 0 && TryWriteInt32(ctx, address, value);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -970,7 +827,7 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
|
||||
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
||||
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1000,7 +857,7 @@ public static class KernelRuntimeCompatExports
|
||||
}
|
||||
|
||||
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
||||
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1201,7 +1058,7 @@ public static class KernelRuntimeCompatExports
|
||||
public static int KernelStopUnloadModule(CpuContext ctx)
|
||||
{
|
||||
var resultAddress = ctx[CpuRegister.R9];
|
||||
if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
|
||||
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1219,7 +1076,7 @@ public static class KernelRuntimeCompatExports
|
||||
{
|
||||
var modulePathAddress = ctx[CpuRegister.Rdi];
|
||||
var resultAddress = ctx[CpuRegister.R9];
|
||||
if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
|
||||
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1376,8 +1233,8 @@ public static class KernelRuntimeCompatExports
|
||||
: 0;
|
||||
var minutesWest = unchecked((int)-offset.TotalMinutes);
|
||||
|
||||
if (!ctx.TryWriteInt32(timezoneAddress, minutesWest) ||
|
||||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), dstSeconds / 60))
|
||||
if (!TryWriteInt32(ctx, timezoneAddress, minutesWest) ||
|
||||
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), dstSeconds / 60))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1387,7 +1244,7 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (dstSecondsAddress != 0 && !ctx.TryWriteInt32(dstSecondsAddress, dstSeconds))
|
||||
if (dstSecondsAddress != 0 && !TryWriteInt32(ctx, dstSecondsAddress, dstSeconds))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1447,7 +1304,7 @@ public static class KernelRuntimeCompatExports
|
||||
_loadedSysmodules.Add(moduleId);
|
||||
}
|
||||
|
||||
if (resultAddress != 0 && !ctx.TryWriteInt32(resultAddress, 0))
|
||||
if (resultAddress != 0 && !TryWriteInt32(ctx, resultAddress, 0))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1537,7 +1394,7 @@ public static class KernelRuntimeCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, module.Handle))
|
||||
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, module.Handle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1574,7 +1431,7 @@ public static class KernelRuntimeCompatExports
|
||||
var writableCount = (int)Math.Min(Math.Min(capacity, (ulong)int.MaxValue), (ulong)handles.Length);
|
||||
for (var i = 0; i < writableCount; i++)
|
||||
{
|
||||
if (!ctx.TryWriteInt32(handlesAddress + (ulong)(i * sizeof(int)), handles[i]))
|
||||
if (!TryWriteInt32(ctx, handlesAddress + (ulong)(i * sizeof(int)), handles[i]))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -1615,42 +1472,26 @@ public static class KernelRuntimeCompatExports
|
||||
return false;
|
||||
}
|
||||
|
||||
const int pageSize = 4096;
|
||||
const int inlineChunkSize = 64;
|
||||
Span<byte> buffer = stackalloc byte[Math.Min(maxLength, 512)];
|
||||
var length = 0;
|
||||
|
||||
for (var offset = 0; offset < maxLength && length < buffer.Length;)
|
||||
var bytes = new List<byte>(Math.Min(maxLength, 64));
|
||||
Span<byte> one = stackalloc byte[1];
|
||||
for (var i = 0; i < maxLength; i++)
|
||||
{
|
||||
var current = address + (ulong)offset;
|
||||
var pageRemaining = pageSize - (int)(current & (pageSize - 1));
|
||||
var chunkSize = Math.Min(
|
||||
buffer.Length - length,
|
||||
Math.Min(maxLength - offset, Math.Min(inlineChunkSize, pageRemaining)));
|
||||
|
||||
if (chunkSize <= 0)
|
||||
if (!ctx.Memory.TryRead(address + (ulong)i, one))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var chunk = buffer.Slice(length, chunkSize);
|
||||
if (!ctx.Memory.TryRead(current, chunk))
|
||||
if (one[0] == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var nulIndex = chunk.IndexOf((byte)0);
|
||||
if (nulIndex >= 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(buffer[..(length + nulIndex)]);
|
||||
value = Encoding.UTF8.GetString(bytes.ToArray());
|
||||
return true;
|
||||
}
|
||||
|
||||
length += chunkSize;
|
||||
offset += chunkSize;
|
||||
bytes.Add(one[0]);
|
||||
}
|
||||
|
||||
return false;
|
||||
value = Encoding.UTF8.GetString(bytes.ToArray());
|
||||
return true;
|
||||
}
|
||||
|
||||
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
||||
@@ -1663,6 +1504,13 @@ public static class KernelRuntimeCompatExports
|
||||
return unchecked(ctx.FsBase + offset);
|
||||
}
|
||||
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static nint AllocateStackChkGuardObject()
|
||||
{
|
||||
try
|
||||
@@ -1859,7 +1707,7 @@ public static class KernelRuntimeCompatExports
|
||||
return null;
|
||||
}
|
||||
|
||||
ReadOnlySpan<byte> stub = stackalloc byte[]
|
||||
Span<byte> stub = stackalloc byte[]
|
||||
{
|
||||
0x0F, 0x31,
|
||||
0x48, 0xC1, 0xE2, 0x20,
|
||||
@@ -1867,14 +1715,7 @@ public static class KernelRuntimeCompatExports
|
||||
0xC3,
|
||||
};
|
||||
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* src = stub)
|
||||
{
|
||||
Buffer.MemoryCopy(src, (void*)stubAddress, stub.Length, stub.Length);
|
||||
}
|
||||
}
|
||||
|
||||
Marshal.Copy(stub.ToArray(), 0, stubAddress, stub.Length);
|
||||
return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress);
|
||||
}
|
||||
catch
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Kernel;
|
||||
@@ -19,22 +21,9 @@ public static class KernelSemaphoreCompatExports
|
||||
public required int MaxCount { get; init; }
|
||||
public int Count { get; set; }
|
||||
public int WaitingThreads { get; set; }
|
||||
public int CancelEpoch { get; set; }
|
||||
public bool Deleted { get; set; }
|
||||
public object Gate { get; } = new();
|
||||
}
|
||||
|
||||
private sealed class SemaphoreWaiter
|
||||
{
|
||||
public required int NeedCount { get; init; }
|
||||
public required int CancelEpochAtBlock { get; init; }
|
||||
|
||||
// Written and read only under the owning semaphore's Gate.
|
||||
public int? Result { get; set; }
|
||||
}
|
||||
|
||||
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "188x57JYp0g",
|
||||
ExportName = "sceKernelCreateSema",
|
||||
@@ -57,12 +46,12 @@ public static class KernelSemaphoreCompatExports
|
||||
initialCount > maxCount ||
|
||||
optionAddress != 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxSemaphoreNameLength, out var name))
|
||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxSemaphoreNameLength, out var name))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||
@@ -71,32 +60,22 @@ public static class KernelSemaphoreCompatExports
|
||||
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||
}
|
||||
|
||||
var state = new KernelSemaphoreState
|
||||
_semaphores[handle] = new KernelSemaphoreState
|
||||
{
|
||||
Name = name,
|
||||
InitialCount = initialCount,
|
||||
MaxCount = maxCount,
|
||||
Count = initialCount,
|
||||
};
|
||||
_semaphores[handle] = state;
|
||||
|
||||
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
||||
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||
{
|
||||
_semaphores.TryRemove(handle, out _);
|
||||
// Handles are sequential and guest-predictable, so a hostile guest can
|
||||
// race a WaitSema onto the handle between publication above and this
|
||||
// rollback. Strand-proof that waiter exactly like DeleteSema does.
|
||||
lock (state.Gate)
|
||||
{
|
||||
state.Deleted = true;
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -112,12 +91,12 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
@@ -126,40 +105,30 @@ public static class KernelSemaphoreCompatExports
|
||||
{
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
if (!ctx.TryReadUInt32(timeoutAddress, out _))
|
||||
if (!TryReadUInt32(ctx, timeoutAddress, out _))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
TraceSemaphore($"wait-timeout handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
var waiter = new SemaphoreWaiter
|
||||
{
|
||||
NeedCount = needCount,
|
||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||
};
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||
ctx,
|
||||
"sceKernelWaitSema",
|
||||
GetSemaphoreWakeKey(handle),
|
||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
|
||||
{
|
||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
semaphore.WaitingThreads++;
|
||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -175,12 +144,12 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (needCount < 1 || needCount > semaphore.MaxCount)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
@@ -188,12 +157,12 @@ public static class KernelSemaphoreCompatExports
|
||||
if (semaphore.Count < needCount)
|
||||
{
|
||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
}
|
||||
|
||||
semaphore.Count -= needCount;
|
||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -209,31 +178,25 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (signalCount <= 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (semaphore.Count > semaphore.MaxCount - signalCount)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
semaphore.Count += signalCount;
|
||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
|
||||
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
||||
// whose needCount exceeds the remaining count stays parked while a smaller
|
||||
// waiter can proceed.
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -249,32 +212,26 @@ public static class KernelSemaphoreCompatExports
|
||||
|
||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (setCount > semaphore.MaxCount)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
if (waitingThreadsAddress != 0 && !ctx.TryWriteUInt32(waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||
if (waitingThreadsAddress != 0 && !TryWriteUInt32(ctx, waitingThreadsAddress, unchecked((uint)semaphore.WaitingThreads)))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
||||
semaphore.CancelEpoch++;
|
||||
// WaitingThreads is NOT zeroed here: each canceled waiter decrements it
|
||||
// exactly once in its wake handler. Zeroing here as well would double-count
|
||||
// and silently absorb the increment of a waiter that parks between this
|
||||
// gate release and the wake-all below.
|
||||
semaphore.WaitingThreads = 0;
|
||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -287,85 +244,68 @@ public static class KernelSemaphoreCompatExports
|
||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||
if (!_semaphores.TryRemove(handle, out var semaphore))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
}
|
||||
|
||||
// Delete succeeds even with blocked waiters; they wake with the deleted
|
||||
// result (the SCE kernel wakes waiters with the EACCES-class code).
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
semaphore.Deleted = true;
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
// Wake handler: runs under the scheduler's guest-thread gate (lock order:
|
||||
// scheduler gate -> semaphore gate). Returns true iff the waiter has a final
|
||||
// result and should be re-readied; false leaves it parked.
|
||||
private static bool TryConsumeBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
lock (semaphore.Gate)
|
||||
{
|
||||
return TryConsumeBlockedSemaWaitLocked(semaphore, waiter);
|
||||
}
|
||||
var value = (int)result;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static bool TryConsumeBlockedSemaWaitLocked(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
if (waiter.Result is not null)
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
return true;
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (semaphore.Deleted)
|
||||
{
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (semaphore.CancelEpoch != waiter.CancelEpochAtBlock)
|
||||
{
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (semaphore.Count >= waiter.NeedCount)
|
||||
{
|
||||
semaphore.Count -= waiter.NeedCount;
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
// Resume handler: runs on the woken guest thread outside the scheduler gate;
|
||||
// its return value becomes the guest's RAX for the resumed sceKernelWaitSema.
|
||||
private static int CompleteBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
lock (semaphore.Gate)
|
||||
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)
|
||||
{
|
||||
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
|
||||
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))
|
||||
{
|
||||
// Nothing readies a parked semaphore waiter without the wake handler
|
||||
// resolving it, so reaching here means the scheduler contract changed.
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][GAP] sema.resume-no-outcome name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count}");
|
||||
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||
return false;
|
||||
}
|
||||
|
||||
return waiter.Result!.Value;
|
||||
if (current[0] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, i);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes[i] = current[0];
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceSemaphore(string message)
|
||||
|
||||
@@ -1,467 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
|
||||
namespace SharpEmu.Libs.LibcStdio;
|
||||
|
||||
public static class LibcStdioExports
|
||||
{
|
||||
private const int MaxPathLength = 4096;
|
||||
private const int MaxModeLength = 16;
|
||||
private const int ReadChunkSize = 1024 * 1024;
|
||||
|
||||
private static readonly ConcurrentDictionary<ulong, FileStream> _fileHandles = new();
|
||||
private static long _nextHandle = 0x1000 - 8;
|
||||
|
||||
private static readonly bool _traceStdio =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STDIO"), "1", StringComparison.Ordinal);
|
||||
|
||||
private const int CtypeTableLowerBound = -128;
|
||||
private const int CtypeTableUpperBound = 255;
|
||||
private const int CtypeTableEntryCount = CtypeTableUpperBound - CtypeTableLowerBound + 1; // 384 entries * 2 bytes = 768 bytes
|
||||
|
||||
// Mirrors the Dinkumware ctype bitmask layout (the CRT Sony's libc is based on;
|
||||
// _Getpctype/_Getptolower/_Getptoupper are Dinkumware accessor names). This is NOT the
|
||||
// MSVC/UCRT layout: e.g. Dinkumware puts digit at 0x20 where UCRT puts control, so
|
||||
// serving a UCRT-shaped table made the game's bundled printf engine misparse every
|
||||
// %-directive (fatal-error messages rendered empty) and made its mcpp preprocessor
|
||||
// treat 'a'-'f' as control characters (UCRT _HEX=0x80 reads as Dinkumware _BB) and
|
||||
// drop them from identifiers ("texture" -> "txtur"). Titles that bundle their own
|
||||
// libc bypass this table entirely (see IsSafeLleLibcExport in DirectExecutionBackend);
|
||||
// this fallback only serves titles that import _Getpctype without shipping one.
|
||||
private const ushort CtypeXDigit = 0x001; // _XD '0'-'9', 'A'-'F', 'a'-'f'
|
||||
private const ushort CtypeUpper = 0x002; // _UP 'A'-'Z'
|
||||
private const ushort CtypeSpace = 0x004; // _SP ' ' (isspace = _CN|_SP|_XS)
|
||||
private const ushort CtypePunct = 0x008; // _PU punctuation
|
||||
private const ushort CtypeLower = 0x010; // _LO 'a'-'z'
|
||||
private const ushort CtypeDigit = 0x020; // _DI '0'-'9'
|
||||
private const ushort CtypeControlSpace = 0x040; // _CN '\t','\n','\v','\f','\r'
|
||||
private const ushort CtypeControl = 0x080; // _BB other control characters
|
||||
private const ushort CtypeBlank = 0x400; // _XB ' ' and '\t'
|
||||
|
||||
private static readonly object _ctypeTableGate = new();
|
||||
private static nint _ctypeTableBase;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xeYO4u7uyJ0",
|
||||
ExportName = "fopen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fopen(CpuContext ctx)
|
||||
{
|
||||
var pathAddress = ctx[CpuRegister.Rdi];
|
||||
var modeAddress = ctx[CpuRegister.Rsi];
|
||||
|
||||
if (pathAddress == 0 || modeAddress == 0 ||
|
||||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) ||
|
||||
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) ||
|
||||
!TryParseFopenMode(mode, out var fileMode, out var fileAccess))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
|
||||
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
|
||||
{
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> PERMISSION_DENIED (read-only path)");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (fileAccess != FileAccess.Read)
|
||||
{
|
||||
var parentDirectory = Path.GetDirectoryName(hostPath);
|
||||
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
||||
{
|
||||
Directory.CreateDirectory(parentDirectory);
|
||||
}
|
||||
}
|
||||
|
||||
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||
if (mode.StartsWith('a') && fileAccess == FileAccess.ReadWrite)
|
||||
{
|
||||
stream.Seek(0, SeekOrigin.End);
|
||||
}
|
||||
|
||||
var handle = unchecked((ulong)Interlocked.Add(ref _nextHandle, 8));
|
||||
_fileHandles[handle] = stream;
|
||||
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X} length={stream.Length}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = handle;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||
{
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return ex is UnauthorizedAccessException
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rQFVBXp-Cxg",
|
||||
ExportName = "fseek",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fseek(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
var offset = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||
var whence = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
|
||||
if (!_fileHandles.TryGetValue(handle, out var stream) || !TryGetSeekOrigin(whence, out var origin))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
stream.Seek(offset, origin);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Qazy8LmXTvw",
|
||||
ExportName = "ftell",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Ftell(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
|
||||
if (!_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)stream.Position);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "uodLYyUip20",
|
||||
ExportName = "fclose",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fclose(CpuContext ctx)
|
||||
{
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
|
||||
if (!_fileHandles.TryRemove(handle, out var stream))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
stream.Dispose();
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "lbB+UlZqVG0",
|
||||
ExportName = "fread",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fread(CpuContext ctx)
|
||||
{
|
||||
var destination = ctx[CpuRegister.Rdi];
|
||||
var elementSize = ctx[CpuRegister.Rsi];
|
||||
var elementCount = ctx[CpuRegister.Rdx];
|
||||
var handle = ctx[CpuRegister.Rcx];
|
||||
|
||||
if (elementSize == 0 || elementCount == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (destination == 0 || !_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var totalRequested = elementSize * elementCount;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested));
|
||||
ulong totalRead = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (totalRead < totalRequested)
|
||||
{
|
||||
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalRead);
|
||||
var read = stream.Read(buffer, 0, request);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (!ctx.Memory.TryWrite(destination + totalRead, buffer.AsSpan(0, read)))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = totalRead / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
totalRead += (ulong)read;
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = totalRead / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
if (_traceStdio)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] fread: handle=0x{handle:X} requested={totalRequested} read={totalRead} pos={stream.Position}");
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = totalRead / elementSize;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "KdP-nULpuGw",
|
||||
ExportName = "fgets",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int Fgets(CpuContext ctx)
|
||||
{
|
||||
var destination = ctx[CpuRegister.Rdi];
|
||||
var maxCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||
var handle = ctx[CpuRegister.Rdx];
|
||||
|
||||
if (destination == 0 || maxCount <= 0 || !_fileHandles.TryGetValue(handle, out var stream))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var buffer = ArrayPool<byte>.Shared.Rent(maxCount - 1);
|
||||
var count = 0;
|
||||
|
||||
try
|
||||
{
|
||||
while (count < maxCount - 1)
|
||||
{
|
||||
var b = stream.ReadByte();
|
||||
if (b < 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
buffer[count++] = (byte)b;
|
||||
if (b == '\n')
|
||||
{
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (count == 0)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
Span<byte> withNul = stackalloc byte[count + 1];
|
||||
buffer.AsSpan(0, count).CopyTo(withNul);
|
||||
withNul[count] = 0;
|
||||
|
||||
if (!ctx.Memory.TryWrite(destination, withNul))
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
ctx[CpuRegister.Rax] = destination;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sUP1hBaouOw",
|
||||
ExportName = "_Getpctype",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
public static int GetPctype(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)EnsureCtypeTable());
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static unsafe nint EnsureCtypeTable()
|
||||
{
|
||||
lock (_ctypeTableGate)
|
||||
{
|
||||
if (_ctypeTableBase != 0)
|
||||
{
|
||||
return _ctypeTableBase;
|
||||
}
|
||||
|
||||
var storage = Marshal.AllocHGlobal(CtypeTableEntryCount * sizeof(ushort));
|
||||
var entries = new Span<ushort>((void*)storage, CtypeTableEntryCount);
|
||||
for (var i = 0; i < CtypeTableEntryCount; i++)
|
||||
{
|
||||
var c = i + CtypeTableLowerBound;
|
||||
entries[i] = c is >= 0 and <= 0x7F ? ComputeCtypeFlags(c) : (ushort)0;
|
||||
}
|
||||
|
||||
// Table is indexed as base[c] for c in [-128, 255], so the pointer handed to the
|
||||
// guest must point at the c == 0 entry, not the start of the allocation.
|
||||
_ctypeTableBase = storage - (CtypeTableLowerBound * sizeof(ushort));
|
||||
return _ctypeTableBase;
|
||||
}
|
||||
}
|
||||
|
||||
private static ushort ComputeCtypeFlags(int c)
|
||||
{
|
||||
var isUpper = c is >= 'A' and <= 'Z';
|
||||
var isLower = c is >= 'a' and <= 'z';
|
||||
var isDigit = c is >= '0' and <= '9';
|
||||
var isHex = isDigit || (c is >= 'A' and <= 'F') || (c is >= 'a' and <= 'f');
|
||||
var isAlnum = isUpper || isLower || isDigit;
|
||||
// In the Dinkumware table '\t'..'\r' carry _CN (isspace and iscntrl both match via
|
||||
// _CN) while plain ' ' carries _SP; keeping them distinct is what keeps isprint(' ')
|
||||
// true and isprint('\t') false.
|
||||
var isControlSpace = c is >= 0x09 and <= 0x0D;
|
||||
var isControl = (c is >= 0x00 and <= 0x08) || (c is >= 0x0E and <= 0x1F) || c == 0x7F;
|
||||
var isPrintable = c is >= 0x20 and <= 0x7E;
|
||||
var isPunct = isPrintable && !isAlnum && c != ' ';
|
||||
|
||||
ushort flags = 0;
|
||||
if (isUpper) flags |= CtypeUpper;
|
||||
if (isLower) flags |= CtypeLower;
|
||||
if (isDigit) flags |= CtypeDigit;
|
||||
if (isHex) flags |= CtypeXDigit;
|
||||
if (c == ' ') flags |= CtypeSpace | CtypeBlank;
|
||||
if (c == '\t') flags |= CtypeBlank;
|
||||
if (isControlSpace) flags |= CtypeControlSpace;
|
||||
if (isPunct) flags |= CtypePunct;
|
||||
if (isControl) flags |= CtypeControl;
|
||||
return flags;
|
||||
}
|
||||
|
||||
private static bool TryParseFopenMode(string mode, out FileMode fileMode, out FileAccess fileAccess)
|
||||
{
|
||||
fileMode = FileMode.Open;
|
||||
fileAccess = FileAccess.Read;
|
||||
|
||||
if (string.IsNullOrEmpty(mode))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var plus = mode.Contains('+');
|
||||
switch (mode[0])
|
||||
{
|
||||
case 'r':
|
||||
fileMode = FileMode.Open;
|
||||
fileAccess = plus ? FileAccess.ReadWrite : FileAccess.Read;
|
||||
return true;
|
||||
|
||||
case 'w':
|
||||
fileMode = FileMode.Create;
|
||||
fileAccess = plus ? FileAccess.ReadWrite : FileAccess.Write;
|
||||
return true;
|
||||
|
||||
case 'a':
|
||||
if (plus)
|
||||
{
|
||||
fileMode = FileMode.OpenOrCreate;
|
||||
fileAccess = FileAccess.ReadWrite;
|
||||
}
|
||||
else
|
||||
{
|
||||
fileMode = FileMode.Append;
|
||||
fileAccess = FileAccess.Write;
|
||||
}
|
||||
|
||||
return true;
|
||||
|
||||
default:
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryGetSeekOrigin(int whence, out SeekOrigin origin)
|
||||
{
|
||||
switch (whence)
|
||||
{
|
||||
case 0:
|
||||
origin = SeekOrigin.Begin;
|
||||
return true;
|
||||
|
||||
case 1:
|
||||
origin = SeekOrigin.Current;
|
||||
return true;
|
||||
|
||||
case 2:
|
||||
origin = SeekOrigin.End;
|
||||
return true;
|
||||
|
||||
default:
|
||||
origin = default;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,33 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Mouse;
|
||||
|
||||
public static class MouseExports
|
||||
{
|
||||
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
|
||||
// as an sceNgs2VoiceGetState alias.
|
||||
[SysAbiExport(
|
||||
Nid = "x8qnXqh-tiM",
|
||||
ExportName = "sceMouseRead",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMouse")]
|
||||
public static int MouseRead(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "RaqxZIf6DvE",
|
||||
ExportName = "sceMouseOpen",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceMouse")]
|
||||
public static int MouseOpen(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
@@ -30,7 +32,7 @@ public static class Http2Exports
|
||||
|
||||
if (poolSize == 0 || maxRequests <= 0)
|
||||
{
|
||||
return ctx.SetReturn(Http2ErrorInvalidArgument);
|
||||
return SetReturn(ctx, Http2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextContextId);
|
||||
@@ -51,11 +53,17 @@ public static class Http2Exports
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!_contexts.TryRemove(id, out _))
|
||||
{
|
||||
return ctx.SetReturn(Http2ErrorInvalidId);
|
||||
return SetReturn(ctx, Http2ErrorInvalidId);
|
||||
}
|
||||
|
||||
TraceHttp2("term", id, 0, 0, 0, 0);
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceHttp2(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
|
||||
@@ -32,7 +33,7 @@ public static class HttpExports
|
||||
var poolSize = ctx[CpuRegister.Rdx];
|
||||
if (poolSize == 0)
|
||||
{
|
||||
return ctx.SetReturn(HttpErrorInvalidValue);
|
||||
return SetReturn(ctx, HttpErrorInvalidValue);
|
||||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextContextId);
|
||||
@@ -52,7 +53,7 @@ public static class HttpExports
|
||||
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!Contexts.ContainsKey(contextId))
|
||||
{
|
||||
return ctx.SetReturn(HttpErrorInvalidId);
|
||||
return SetReturn(ctx, HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
var userAgentAddress = ctx[CpuRegister.Rsi];
|
||||
@@ -74,8 +75,8 @@ public static class HttpExports
|
||||
{
|
||||
var templateId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return Templates.TryRemove(templateId, out _)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(HttpErrorInvalidId);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -88,7 +89,7 @@ public static class HttpExports
|
||||
var contextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!Contexts.TryRemove(contextId, out _))
|
||||
{
|
||||
return ctx.SetReturn(HttpErrorInvalidId);
|
||||
return SetReturn(ctx, HttpErrorInvalidId);
|
||||
}
|
||||
|
||||
foreach (var pair in Templates)
|
||||
@@ -99,7 +100,13 @@ public static class HttpExports
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceHttp(string operation, int id, ulong arg0, ulong arg1, ulong arg2, ulong arg3)
|
||||
|
||||
@@ -57,27 +57,27 @@ public static class NetCtlExports
|
||||
var natInfoAddress = ctx[CpuRegister.Rdi];
|
||||
if (natInfoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
}
|
||||
|
||||
Span<byte> natInfo = stackalloc byte[NatInfoSize];
|
||||
if (!ctx.Memory.TryRead(natInfoAddress, natInfo))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
var size = BinaryPrimitives.ReadUInt32LittleEndian(natInfo[..sizeof(uint)]);
|
||||
if (size != NatInfoSize)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT, typeof(long));
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
BinaryPrimitives.WriteInt32LittleEndian(natInfo[4..], 1);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(natInfo[8..], 3);
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(natInfo[12..], 0x7F000001);
|
||||
return ctx.Memory.TryWrite(natInfoAddress, natInfo)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -87,7 +87,7 @@ public static class NetCtlExports
|
||||
LibraryName = "libSceNetCtl")]
|
||||
public static int NetCtlCheckCallback(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -100,14 +100,14 @@ public static class NetCtlExports
|
||||
var stateAddress = ctx[CpuRegister.Rdi];
|
||||
if (stateAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
}
|
||||
|
||||
Span<byte> stateBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(stateBytes, 0);
|
||||
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -122,7 +122,7 @@ public static class NetCtlExports
|
||||
var callbackIdAddress = ctx[CpuRegister.Rdx];
|
||||
if (function == 0 || callbackIdAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
}
|
||||
|
||||
lock (CallbackGate)
|
||||
@@ -130,20 +130,20 @@ public static class NetCtlExports
|
||||
var callbackId = Array.FindIndex(Callbacks, static callback => callback.Function == 0);
|
||||
if (callbackId < 0)
|
||||
{
|
||||
return ctx.SetReturn(NetCtlErrorNoSpace, typeof(long));
|
||||
return SetReturn(ctx, NetCtlErrorNoSpace);
|
||||
}
|
||||
|
||||
Span<byte> callbackIdBytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(callbackIdBytes, unchecked((uint)callbackId));
|
||||
if (!ctx.Memory.TryWrite(callbackIdAddress, callbackIdBytes))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
Callbacks[callbackId] = new CallbackRegistration(function, argument);
|
||||
}
|
||||
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_OK, typeof(long));
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -157,7 +157,7 @@ public static class NetCtlExports
|
||||
var infoAddress = ctx[CpuRegister.Rsi];
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(NetCtlErrorInvalidAddress, typeof(long));
|
||||
return SetReturn(ctx, NetCtlErrorInvalidAddress);
|
||||
}
|
||||
|
||||
return code switch
|
||||
@@ -177,7 +177,7 @@ public static class NetCtlExports
|
||||
NetCtlInfoHttpProxyConfig => WriteUInt32(ctx, infoAddress, 0),
|
||||
NetCtlInfoHttpProxyServer => WriteAsciiZ(ctx, infoAddress, string.Empty, 256),
|
||||
NetCtlInfoHttpProxyPort => WriteUInt16(ctx, infoAddress, 0),
|
||||
_ => ctx.SetReturn(NetCtlErrorNotConnected, typeof(long)),
|
||||
_ => SetReturn(ctx, NetCtlErrorNotConnected),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -185,8 +185,8 @@ public static class NetCtlExports
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[count];
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int WriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
@@ -194,8 +194,8 @@ public static class NetCtlExports
|
||||
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int WriteUInt16(CpuContext ctx, ulong address, ushort value)
|
||||
@@ -203,8 +203,8 @@ public static class NetCtlExports
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int WriteAsciiZ(CpuContext ctx, ulong address, string value, int byteCount)
|
||||
@@ -217,7 +217,13 @@ public static class NetCtlExports
|
||||
}
|
||||
|
||||
return ctx.Memory.TryWrite(address, bytes)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Text;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
@@ -33,7 +35,7 @@ public static class NetExports
|
||||
{
|
||||
_initialized = true;
|
||||
TraceNet("init", 0, 0, 0, 0);
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -47,7 +49,7 @@ public static class NetExports
|
||||
_pools.Clear();
|
||||
_resolvers.Clear();
|
||||
TraceNet("term", 0, 0, 0, 0);
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -63,7 +65,7 @@ public static class NetExports
|
||||
|
||||
if (size <= 0)
|
||||
{
|
||||
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||
@@ -88,11 +90,11 @@ public static class NetExports
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!_pools.TryRemove(id, out _))
|
||||
{
|
||||
return ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||
return SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||
}
|
||||
|
||||
TraceNet("pool.destroy", id, 0, 0, _initialized ? 1UL : 0UL);
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -107,7 +109,7 @@ public static class NetExports
|
||||
var flags = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||
if (flags != 0)
|
||||
{
|
||||
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||
}
|
||||
|
||||
var name = TryReadUtf8Z(ctx, nameAddress, MaxNameLength, out var value)
|
||||
@@ -129,8 +131,8 @@ public static class NetExports
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
return _resolvers.TryRemove(id, out _)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -144,19 +146,25 @@ public static class NetExports
|
||||
var statusAddress = ctx[CpuRegister.Rsi];
|
||||
if (statusAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(NetErrorInvalidArgument);
|
||||
return SetReturn(ctx, NetErrorInvalidArgument);
|
||||
}
|
||||
|
||||
if (!_resolvers.TryGetValue(id, out var resolver))
|
||||
{
|
||||
return ctx.SetReturn(NetErrorBadFileDescriptor);
|
||||
return SetReturn(ctx, NetErrorBadFileDescriptor);
|
||||
}
|
||||
|
||||
Span<byte> status = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(status, resolver.LastError);
|
||||
return ctx.Memory.TryWrite(statusAddress, status)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static bool TryReadUtf8Z(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Network;
|
||||
@@ -26,7 +28,7 @@ public static class SslExports
|
||||
var poolSize = ctx[CpuRegister.Rdi];
|
||||
if (poolSize == 0)
|
||||
{
|
||||
return ctx.SetReturn(SslErrorOutOfSize);
|
||||
return SetReturn(ctx, SslErrorOutOfSize);
|
||||
}
|
||||
|
||||
var id = Interlocked.Increment(ref _nextContextId);
|
||||
@@ -47,11 +49,11 @@ public static class SslExports
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
if (!_contexts.TryRemove(id, out _))
|
||||
{
|
||||
return ctx.SetReturn(SslErrorInvalidId);
|
||||
return SetReturn(ctx, SslErrorInvalidId);
|
||||
}
|
||||
|
||||
TraceSsl("term", id, 0);
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -63,7 +65,13 @@ public static class SslExports
|
||||
{
|
||||
var id = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
TraceSsl("close", id, 0);
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceSsl(string operation, int id, ulong arg0)
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Ngs2;
|
||||
|
||||
@@ -13,7 +14,7 @@ public static class Ngs2Exports
|
||||
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 = 0x1000;
|
||||
private const ulong HandleStorageSize = 0x20;
|
||||
private const int RenderBufferInfoSize = 0x18;
|
||||
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
|
||||
|
||||
@@ -38,13 +39,13 @@ public static class Ngs2Exports
|
||||
var outHandleAddress = ctx[CpuRegister.Rdx];
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 1, ownerHandle: 0, out var handle) ||
|
||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (StateGate)
|
||||
@@ -52,7 +53,7 @@ public static class Ngs2Exports
|
||||
Systems[handle] = new SystemState(unchecked((uint)Interlocked.Increment(ref _nextUid)));
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -67,7 +68,7 @@ public static class Ngs2Exports
|
||||
{
|
||||
if (!Systems.Remove(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
|
||||
var rackHandles = Racks
|
||||
@@ -80,7 +81,7 @@ public static class Ngs2Exports
|
||||
}
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -97,19 +98,19 @@ public static class Ngs2Exports
|
||||
{
|
||||
if (!Systems.ContainsKey(systemHandle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
}
|
||||
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 2, systemHandle, out var handle) ||
|
||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (StateGate)
|
||||
@@ -117,7 +118,7 @@ public static class Ngs2Exports
|
||||
Racks[handle] = new RackState(systemHandle, rackId);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -132,13 +133,13 @@ public static class Ngs2Exports
|
||||
{
|
||||
if (!Racks.ContainsKey(handle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||
}
|
||||
|
||||
RemoveRackLocked(handle);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -155,7 +156,7 @@ public static class Ngs2Exports
|
||||
{
|
||||
if (!Racks.ContainsKey(rackHandle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidRackHandle);
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidRackHandle);
|
||||
}
|
||||
|
||||
var existing = Voices.FirstOrDefault(
|
||||
@@ -163,20 +164,20 @@ public static class Ngs2Exports
|
||||
if (existing.Key != 0)
|
||||
{
|
||||
return ctx.TryWriteUInt64(outHandleAddress, existing.Key)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
if (outHandleAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidOutAddress);
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidOutAddress);
|
||||
}
|
||||
|
||||
if (!TryCreateHandle(ctx, type: 4, rackHandle, out var handle) ||
|
||||
!ctx.TryWriteUInt64(outHandleAddress, handle))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
lock (StateGate)
|
||||
@@ -184,7 +185,7 @@ public static class Ngs2Exports
|
||||
Voices[handle] = new VoiceState(rackHandle, voiceIndex);
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -196,19 +197,12 @@ public static class Ngs2Exports
|
||||
{
|
||||
lock (StateGate)
|
||||
{
|
||||
return ctx.SetReturn(
|
||||
Voices.ContainsKey(ctx[CpuRegister.Rdi])
|
||||
? 0
|
||||
: OrbisNgs2ErrorInvalidVoiceHandle);
|
||||
return SetReturn(
|
||||
ctx,
|
||||
Voices.ContainsKey(ctx[CpuRegister.Rdi]) ? 0 : OrbisNgs2ErrorInvalidVoiceHandle);
|
||||
}
|
||||
}
|
||||
|
||||
// The earlier "alt NID" guesses here were wrong: an NID is the aerolib hash of one
|
||||
// symbol name, and hashing scripts/ps5_names.txt shows the previous alt bindings
|
||||
// actually belonged to sceImeUpdate (-4GCfYdNF1s), sceMouseRead (x8qnXqh-tiM) and
|
||||
// sceSystemGestureUpdateAllTouchRecognizer (wPJGwI2RM2I) — Quake's audio thread was
|
||||
// receiving an NGS2 error from what it thought was sceImeUpdate. Those now live in
|
||||
// their real libraries; the NIDs below are verified against the hash.
|
||||
[SysAbiExport(
|
||||
Nid = "AbYvTOZ8Pts",
|
||||
ExportName = "sceNgs2VoiceRunCommands",
|
||||
@@ -216,27 +210,6 @@ public static class Ngs2Exports
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "-TOuuAQ-buE",
|
||||
ExportName = "sceNgs2VoiceGetState",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "rEh728kXk3w",
|
||||
ExportName = "sceNgs2VoiceGetStateFlags",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2VoiceGetStateFlags(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xa8oL9dmXkM",
|
||||
ExportName = "sceNgs2PanInit",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNgs2")]
|
||||
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "i0VnXM-C9fc",
|
||||
ExportName = "sceNgs2SystemRender",
|
||||
@@ -251,13 +224,13 @@ public static class Ngs2Exports
|
||||
{
|
||||
if (!Systems.ContainsKey(systemHandle))
|
||||
{
|
||||
return ctx.SetReturn(OrbisNgs2ErrorInvalidSystemHandle);
|
||||
return SetReturn(ctx, OrbisNgs2ErrorInvalidSystemHandle);
|
||||
}
|
||||
}
|
||||
|
||||
if (bufferInfoCount != 0 && bufferInfoAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
for (uint i = 0; i < bufferInfoCount; i++)
|
||||
@@ -266,14 +239,14 @@ public static class Ngs2Exports
|
||||
if (!ctx.TryReadUInt64(entryAddress, out var bufferAddress) ||
|
||||
!ctx.TryReadUInt64(entryAddress + 8, out var bufferSize))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
if (bufferAddress != 0 && bufferSize != 0)
|
||||
{
|
||||
if (bufferSize > MaximumRenderBufferSize || !TryClearGuestBuffer(ctx, bufferAddress, bufferSize))
|
||||
{
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -285,11 +258,12 @@ public static class Ngs2Exports
|
||||
$"[LOADER][TRACE] ngs2.render#{count} system=0x{systemHandle:X16} buffers={bufferInfoCount}");
|
||||
}
|
||||
|
||||
return ctx.SetReturn(0);
|
||||
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;
|
||||
@@ -297,18 +271,11 @@ public static class Ngs2Exports
|
||||
|
||||
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);
|
||||
if (!ctx.Memory.TryWrite(handle, data))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Offset 0 doubles as a vtable slot for guest code that treats this handle as a C++
|
||||
// object; stamp it with the shared dummy vtable instead of a self-referential value.
|
||||
KernelMemoryCompatExports.TryWriteDummyVtable(ctx, handle);
|
||||
return true;
|
||||
return ctx.Memory.TryWrite(handle, data);
|
||||
}
|
||||
|
||||
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
||||
@@ -346,4 +313,10 @@ public static class Ngs2Exports
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_NGS2"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,12 +26,12 @@ public static class NpEntitlementAccessExports
|
||||
clear.Clear();
|
||||
if (!ctx.Memory.TryWrite(bootParam, clear))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
TraceNpEntitlementAccess($"initialize init=0x{initParam:X16} boot=0x{bootParam:X16}");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -48,14 +48,20 @@ public static class NpEntitlementAccessExports
|
||||
emptyList.Clear();
|
||||
if (!ctx.Memory.TryWrite(listAddress, emptyList))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
}
|
||||
|
||||
TraceNpEntitlementAccess(
|
||||
$"get_addcont_info_list service=0x{ctx[CpuRegister.Rdi]:X16} list=0x{listAddress:X16} " +
|
||||
$"max={ctx[CpuRegister.Rdx]} flags=0x{ctx[CpuRegister.Rcx]:X16} -> empty");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
private static void TraceNpEntitlementAccess(string message)
|
||||
|
||||
@@ -33,30 +33,6 @@ public static class NpManagerExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Offline profile: the online id payload is left untouched and the call
|
||||
// reports success, matching the other offline NpManager stubs here.
|
||||
[SysAbiExport(
|
||||
Nid = "XDncXQIJUSk",
|
||||
ExportName = "sceNpGetOnlineId",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpManager")]
|
||||
public static int NpGetOnlineId(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "e-ZuhGEoeC4",
|
||||
ExportName = "sceNpGetNpReachabilityState",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpManager")]
|
||||
public static int NpGetNpReachabilityState(CpuContext ctx)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "VfRSmPmj8Q8",
|
||||
ExportName = "sceNpRegisterStateCallback",
|
||||
@@ -100,14 +76,14 @@ public static class NpManagerExports
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (stateAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> stateBytes = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(stateBytes, 1);
|
||||
return ctx.Memory.TryWrite(stateAddress, stateBytes)
|
||||
? ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
: SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -121,7 +97,7 @@ public static class NpManagerExports
|
||||
var titleSecretAddress = ctx[CpuRegister.Rsi];
|
||||
if (titleIdAddress == 0 || titleSecretAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> titleId = stackalloc byte[NpTitleIdSize];
|
||||
@@ -129,11 +105,17 @@ public static class NpManagerExports
|
||||
if (!ctx.Memory.TryRead(titleIdAddress, titleId) ||
|
||||
!ctx.Memory.TryRead(titleSecretAddress, titleSecret))
|
||||
{
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
TraceNp($"set_np_title_id title='{ReadTitleId(titleId)}'");
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
|
||||
private static string ReadTitleId(ReadOnlySpan<byte> bytes)
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
|
||||
namespace SharpEmu.Libs.Np;
|
||||
|
||||
public static class NpTrophy2Exports
|
||||
{
|
||||
private static int _nextContext = 1;
|
||||
private static int _nextHandle = 1;
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Bagshr7OQ6Q",
|
||||
ExportName = "sceNpTrophy2CreateContext",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2CreateContext(CpuContext ctx)
|
||||
{
|
||||
return WriteIdAndReturn(ctx, ctx[CpuRegister.Rdi], ref _nextContext);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "Gz1rmUZpROM",
|
||||
ExportName = "sceNpTrophy2CreateHandle",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2CreateHandle(CpuContext ctx)
|
||||
{
|
||||
return WriteIdAndReturn(ctx, ctx[CpuRegister.Rdi], ref _nextHandle);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sysY2FHYff4",
|
||||
ExportName = "sceNpTrophy2DestroyContext",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2DestroyContext(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "d8P11CI40KE",
|
||||
ExportName = "sceNpTrophy2DestroyHandle",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2DestroyHandle(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "fYapWA9xVmA",
|
||||
ExportName = "sceNpTrophy2AbortHandle",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2AbortHandle(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "bIDov3wBu5Q",
|
||||
ExportName = "sceNpTrophy2RegisterContext",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2RegisterContext(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "sUXGfNMalIo",
|
||||
ExportName = "sceNpTrophy2RegisterUnlockCallback",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2RegisterUnlockCallback(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "wVqxM58sIKs",
|
||||
ExportName = "sceNpTrophy2UnregisterUnlockCallback",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2UnregisterUnlockCallback(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "EHQEDVXZ0TI",
|
||||
ExportName = "sceNpTrophy2ShowTrophyList",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceNpTrophy2")]
|
||||
public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx);
|
||||
|
||||
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
|
||||
{
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> idBytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(idBytes, nextId);
|
||||
if (!ctx.Memory.TryWrite(outAddress, idBytes))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
nextId++;
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
private static int ReturnOk(CpuContext ctx) => SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Np;
|
||||
|
||||
@@ -21,13 +22,13 @@ public static class NpUniversalDataSystemExports
|
||||
var parameterAddress = ctx[CpuRegister.Rdi];
|
||||
if (parameterAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(NpUniversalDataSystemErrorInvalidArgument, typeof(long));
|
||||
return SetReturn(ctx, NpUniversalDataSystemErrorInvalidArgument);
|
||||
}
|
||||
|
||||
Span<byte> parameters = stackalloc byte[16];
|
||||
return ctx.Memory.TryRead(parameterAddress, parameters)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -40,14 +41,14 @@ public static class NpUniversalDataSystemExports
|
||||
var contextAddress = ctx[CpuRegister.Rdi];
|
||||
if (contextAddress == 0)
|
||||
{
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
Span<byte> context = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(context, 1);
|
||||
return ctx.Memory.TryWrite(contextAddress, context)
|
||||
? ctx.SetReturn(0, typeof(long))
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -58,13 +59,13 @@ public static class NpUniversalDataSystemExports
|
||||
public static int NpUniversalDataSystemCreateHandle(CpuContext ctx)
|
||||
{
|
||||
var handle = Interlocked.Increment(ref _nextHandle);
|
||||
if (ctx.TryWriteInt32(ctx[CpuRegister.Rdi], handle, checkNil: true) ||
|
||||
ctx.TryWriteInt32(ctx[CpuRegister.Rsi], handle, checkNil: true))
|
||||
if (TryWriteInt32(ctx, ctx[CpuRegister.Rdi], handle) ||
|
||||
TryWriteInt32(ctx, ctx[CpuRegister.Rsi], handle))
|
||||
{
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT, typeof(long));
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -74,16 +75,24 @@ public static class NpUniversalDataSystemExports
|
||||
LibraryName = "libSceNpUniversalDataSystem")]
|
||||
public static int NpUniversalDataSystemRegisterContext(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "AUIHb7jUX3I",
|
||||
ExportName = "sceNpUniversalDataSystemDestroyHandle",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libSceNpUniversalDataSystem")]
|
||||
public static int NpUniversalDataSystemDestroyHandle(CpuContext ctx)
|
||||
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||
{
|
||||
return ctx.SetReturn(0, typeof(long));
|
||||
if (address == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return ctx.Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)(long)result);
|
||||
return result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Np;
|
||||
@@ -23,12 +25,12 @@ public static class NpWebApi2Exports
|
||||
|
||||
if (httpContextId <= 0 || poolSize == 0)
|
||||
{
|
||||
return ctx.SetReturn(NpWebApi2ErrorInvalidArgument);
|
||||
return SetReturn(ctx, NpWebApi2ErrorInvalidArgument);
|
||||
}
|
||||
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
TraceNpWebApi2("init", httpContextId, poolSize);
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -41,7 +43,13 @@ public static class NpWebApi2Exports
|
||||
var libraryContextId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||
Interlocked.Exchange(ref _initialized, 0);
|
||||
TraceNpWebApi2("term", libraryContextId, 0);
|
||||
return ctx.SetReturn(0);
|
||||
return SetReturn(ctx, 0);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceNpWebApi2(string operation, int id, ulong arg0)
|
||||
|
||||
@@ -1,437 +0,0 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.Libs.Pad;
|
||||
|
||||
/// <summary>
|
||||
/// Reads a DualSense controller over raw HID on a background thread.
|
||||
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
|
||||
/// activated by requesting feature report 0x05), with hot-plug retry.
|
||||
/// </summary>
|
||||
internal static class DualSenseReader
|
||||
{
|
||||
private const ushort SonyVendorId = 0x054C;
|
||||
private const ushort DualSenseProductId = 0x0CE6;
|
||||
private const ushort DualSenseEdgeProductId = 0x0DF2;
|
||||
|
||||
private static readonly object Gate = new();
|
||||
private static PadState _state;
|
||||
private static bool _started;
|
||||
|
||||
// Output (rumble/lightbar) state, all guarded by Gate.
|
||||
private static string? _devicePath;
|
||||
private static bool _bluetooth;
|
||||
private static bool _outputReady;
|
||||
private static bool _lightbarSetupPending;
|
||||
private static byte _outputSequence;
|
||||
private static FileStream? _outputStream;
|
||||
private static byte _motorLeft;
|
||||
private static byte _motorRight;
|
||||
private static byte _lightbarRed;
|
||||
private static byte _lightbarGreen;
|
||||
private static byte _lightbarBlue = 64; // PS-style blue default
|
||||
private static byte _playerLeds = 0x04; // center LED = player 1
|
||||
|
||||
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||
internal static void EnsureStarted()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (Gate)
|
||||
{
|
||||
if (_started)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_started = true;
|
||||
var thread = new Thread(ReadLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "DualSenseReader",
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
}
|
||||
|
||||
internal static bool TryGetState(out PadState state)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
state = _state;
|
||||
}
|
||||
|
||||
return state.Connected;
|
||||
}
|
||||
|
||||
private static void SetState(in PadState state)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_state = state;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Sets rumble; large = left/strong motor, small = right/weak.</summary>
|
||||
internal static void SetRumble(byte largeMotor, byte smallMotor)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (_motorLeft == largeMotor && _motorRight == smallMotor)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_motorLeft = largeMotor;
|
||||
_motorRight = smallMotor;
|
||||
SendOutputLocked();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void SetLightbar(byte red, byte green, byte blue)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
if (_lightbarRed == red && _lightbarGreen == green && _lightbarBlue == blue)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_lightbarRed = red;
|
||||
_lightbarGreen = green;
|
||||
_lightbarBlue = blue;
|
||||
SendOutputLocked();
|
||||
}
|
||||
}
|
||||
|
||||
internal static void ResetLightbar() => SetLightbar(0, 0, 64);
|
||||
|
||||
private static void OnDeviceIdentified(string path, bool bluetooth)
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_devicePath = path;
|
||||
_bluetooth = bluetooth;
|
||||
_outputReady = true;
|
||||
_lightbarSetupPending = true;
|
||||
// Announce ourselves on the hardware: default lightbar + player 1 LED.
|
||||
SendOutputLocked();
|
||||
}
|
||||
}
|
||||
|
||||
private static void OnDeviceLost()
|
||||
{
|
||||
lock (Gate)
|
||||
{
|
||||
_devicePath = null;
|
||||
_outputReady = false;
|
||||
_motorLeft = 0;
|
||||
_motorRight = 0;
|
||||
_outputStream?.Dispose();
|
||||
_outputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static void SendOutputLocked()
|
||||
{
|
||||
if (!_outputReady || _devicePath is null)
|
||||
{
|
||||
return; // flushed by OnDeviceIdentified once connected
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
if (_outputStream is null)
|
||||
{
|
||||
var handle = HidNative.CreateFile(
|
||||
_devicePath,
|
||||
HidNative.GenericRead | HidNative.GenericWrite,
|
||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
||||
0, HidNative.OpenExisting, 0, 0);
|
||||
if (handle.IsInvalid)
|
||||
{
|
||||
handle.Dispose();
|
||||
return; // read-only device access: outputs unavailable
|
||||
}
|
||||
|
||||
_outputStream = new FileStream(handle, FileAccess.Write, bufferSize: 1);
|
||||
}
|
||||
|
||||
var report = BuildOutputReportLocked();
|
||||
_outputStream.Write(report, 0, report.Length);
|
||||
_outputStream.Flush();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
_outputStream?.Dispose();
|
||||
_outputStream = null;
|
||||
}
|
||||
}
|
||||
|
||||
private static byte[] BuildOutputReportLocked()
|
||||
{
|
||||
// Common 47-byte output payload (offsets per the DualSense output
|
||||
// report layout, same as Linux hid-playstation).
|
||||
Span<byte> common = stackalloc byte[47];
|
||||
common[0] = 0x03; // valid_flag0: compatible vibration + haptics select
|
||||
common[1] = 0x04 | 0x10; // valid_flag1: lightbar + player indicator
|
||||
common[2] = _motorRight; // right (weak) motor
|
||||
common[3] = _motorLeft; // left (strong) motor
|
||||
if (_lightbarSetupPending)
|
||||
{
|
||||
common[38] |= 0x02; // valid_flag2: lightbar setup control enable
|
||||
common[41] = 0x01; // lightbar_setup: light on
|
||||
_lightbarSetupPending = false;
|
||||
}
|
||||
|
||||
common[43] = _playerLeds;
|
||||
common[44] = _lightbarRed;
|
||||
common[45] = _lightbarGreen;
|
||||
common[46] = _lightbarBlue;
|
||||
|
||||
if (!_bluetooth)
|
||||
{
|
||||
var usbReport = new byte[48];
|
||||
usbReport[0] = 0x02;
|
||||
common.CopyTo(usbReport.AsSpan(1));
|
||||
return usbReport;
|
||||
}
|
||||
|
||||
// Bluetooth: 0x31 wrapper with sequence tag and CRC32 over a 0xA2
|
||||
// seed byte plus the first 74 report bytes.
|
||||
var btReport = new byte[78];
|
||||
btReport[0] = 0x31;
|
||||
btReport[1] = (byte)((_outputSequence & 0x0F) << 4);
|
||||
_outputSequence = (byte)((_outputSequence + 1) & 0x0F);
|
||||
btReport[2] = 0x10;
|
||||
common.CopyTo(btReport.AsSpan(3));
|
||||
var crc = Crc32(0xA2, btReport.AsSpan(0, 74));
|
||||
btReport[74] = (byte)crc;
|
||||
btReport[75] = (byte)(crc >> 8);
|
||||
btReport[76] = (byte)(crc >> 16);
|
||||
btReport[77] = (byte)(crc >> 24);
|
||||
return btReport;
|
||||
}
|
||||
|
||||
private static uint Crc32(byte seed, ReadOnlySpan<byte> data)
|
||||
{
|
||||
var crc = Crc32Update(0xFFFFFFFFu, seed);
|
||||
foreach (var value in data)
|
||||
{
|
||||
crc = Crc32Update(crc, value);
|
||||
}
|
||||
|
||||
return ~crc;
|
||||
}
|
||||
|
||||
private static uint Crc32Update(uint crc, byte value)
|
||||
{
|
||||
crc ^= value;
|
||||
for (var bit = 0; bit < 8; bit++)
|
||||
{
|
||||
crc = (crc >> 1) ^ (0xEDB88320u & (uint)-(int)(crc & 1));
|
||||
}
|
||||
|
||||
return crc;
|
||||
}
|
||||
|
||||
private static void ReadLoop()
|
||||
{
|
||||
var announcedConnect = false;
|
||||
while (true)
|
||||
{
|
||||
SafeFileHandle? handle = null;
|
||||
try
|
||||
{
|
||||
handle = OpenDualSense(out var devicePath);
|
||||
if (handle is null || devicePath is null)
|
||||
{
|
||||
SetState(default);
|
||||
announcedConnect = false;
|
||||
Thread.Sleep(1000);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Bluetooth quirk: the DualSense sends a simplified report
|
||||
// until feature report 0x05 is requested, which switches it
|
||||
// to the full 0x31 input report. Harmless over USB.
|
||||
var feature = new byte[41];
|
||||
feature[0] = 0x05;
|
||||
_ = HidNative.HidD_GetFeature(handle, feature, feature.Length);
|
||||
|
||||
if (!announcedConnect)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] DualSense controller connected.");
|
||||
announcedConnect = true;
|
||||
}
|
||||
|
||||
using var stream = new FileStream(handle, FileAccess.Read, bufferSize: 1);
|
||||
handle = null; // stream owns it now
|
||||
var buffer = new byte[256];
|
||||
var transportKnown = false;
|
||||
while (true)
|
||||
{
|
||||
var read = stream.Read(buffer, 0, buffer.Length);
|
||||
if (read <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (TryParseReport(buffer.AsSpan(0, read), out var state))
|
||||
{
|
||||
if (!transportKnown)
|
||||
{
|
||||
// The first parsed report tells us the transport,
|
||||
// which the output (rumble/lightbar) path needs.
|
||||
transportKnown = true;
|
||||
OnDeviceIdentified(devicePath, bluetooth: buffer[0] == 0x31);
|
||||
}
|
||||
|
||||
SetState(state);
|
||||
}
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Unplugged or read error: fall through and retry.
|
||||
}
|
||||
finally
|
||||
{
|
||||
handle?.Dispose();
|
||||
}
|
||||
|
||||
if (announcedConnect)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][INFO] DualSense controller disconnected.");
|
||||
announcedConnect = false;
|
||||
}
|
||||
|
||||
OnDeviceLost();
|
||||
SetState(default);
|
||||
Thread.Sleep(1000);
|
||||
}
|
||||
}
|
||||
|
||||
private static SafeFileHandle? OpenDualSense(out string? devicePath)
|
||||
{
|
||||
devicePath = null;
|
||||
foreach (var path in HidNative.EnumerateHidDevicePaths())
|
||||
{
|
||||
// Open without access rights just to query VID/PID.
|
||||
using var probe = HidNative.CreateFile(
|
||||
path, 0, HidNative.FileShareRead | HidNative.FileShareWrite, 0, HidNative.OpenExisting, 0, 0);
|
||||
if (probe.IsInvalid)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var attributes = new HidNative.HiddAttributes { Size = 12 };
|
||||
if (!HidNative.HidD_GetAttributes(probe, ref attributes) ||
|
||||
attributes.VendorId != SonyVendorId ||
|
||||
(attributes.ProductId != DualSenseProductId && attributes.ProductId != DualSenseEdgeProductId))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
// Read+write so feature reports work; fall back to read-only.
|
||||
var handle = HidNative.CreateFile(
|
||||
path,
|
||||
HidNative.GenericRead | HidNative.GenericWrite,
|
||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
||||
0, HidNative.OpenExisting, 0, 0);
|
||||
if (handle.IsInvalid)
|
||||
{
|
||||
handle.Dispose();
|
||||
handle = HidNative.CreateFile(
|
||||
path,
|
||||
HidNative.GenericRead,
|
||||
HidNative.FileShareRead | HidNative.FileShareWrite,
|
||||
0, HidNative.OpenExisting, 0, 0);
|
||||
}
|
||||
|
||||
if (!handle.IsInvalid)
|
||||
{
|
||||
devicePath = path;
|
||||
return handle;
|
||||
}
|
||||
|
||||
handle.Dispose();
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static bool TryParseReport(ReadOnlySpan<byte> report, out PadState state)
|
||||
{
|
||||
// USB: report id 0x01, payload starts at [1].
|
||||
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
|
||||
int offset;
|
||||
if (report.Length >= 11 && report[0] == 0x01)
|
||||
{
|
||||
offset = 1;
|
||||
}
|
||||
else if (report.Length >= 12 && report[0] == 0x31)
|
||||
{
|
||||
offset = 2;
|
||||
}
|
||||
else
|
||||
{
|
||||
state = default;
|
||||
return false;
|
||||
}
|
||||
|
||||
var leftX = report[offset + 0];
|
||||
var leftY = report[offset + 1];
|
||||
var rightX = report[offset + 2];
|
||||
var rightY = report[offset + 3];
|
||||
var l2 = report[offset + 4];
|
||||
var r2 = report[offset + 5];
|
||||
var buttons0 = report[offset + 7];
|
||||
var buttons1 = report[offset + 8];
|
||||
var buttons2 = report[offset + 9];
|
||||
|
||||
uint buttons = 0;
|
||||
buttons |= (buttons0 & 0x10) != 0 ? OrbisPadButton.Square : 0;
|
||||
buttons |= (buttons0 & 0x20) != 0 ? OrbisPadButton.Cross : 0;
|
||||
buttons |= (buttons0 & 0x40) != 0 ? OrbisPadButton.Circle : 0;
|
||||
buttons |= (buttons0 & 0x80) != 0 ? OrbisPadButton.Triangle : 0;
|
||||
buttons |= HatToButtons(buttons0 & 0x0F);
|
||||
buttons |= (buttons1 & 0x01) != 0 ? OrbisPadButton.L1 : 0;
|
||||
buttons |= (buttons1 & 0x02) != 0 ? OrbisPadButton.R1 : 0;
|
||||
buttons |= (buttons1 & 0x04) != 0 ? OrbisPadButton.L2 : 0;
|
||||
buttons |= (buttons1 & 0x08) != 0 ? OrbisPadButton.R2 : 0;
|
||||
buttons |= (buttons1 & 0x20) != 0 ? OrbisPadButton.Options : 0;
|
||||
buttons |= (buttons1 & 0x40) != 0 ? OrbisPadButton.L3 : 0;
|
||||
buttons |= (buttons1 & 0x80) != 0 ? OrbisPadButton.R3 : 0;
|
||||
buttons |= (buttons2 & 0x02) != 0 ? OrbisPadButton.TouchPad : 0;
|
||||
|
||||
state = new PadState(
|
||||
Connected: true,
|
||||
Buttons: buttons,
|
||||
LeftX: leftX,
|
||||
LeftY: leftY,
|
||||
RightX: rightX,
|
||||
RightY: rightY,
|
||||
L2: l2,
|
||||
R2: r2);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static uint HatToButtons(int hat) => hat switch
|
||||
{
|
||||
0 => OrbisPadButton.Up,
|
||||
1 => OrbisPadButton.Up | OrbisPadButton.Right,
|
||||
2 => OrbisPadButton.Right,
|
||||
3 => OrbisPadButton.Right | OrbisPadButton.Down,
|
||||
4 => OrbisPadButton.Down,
|
||||
5 => OrbisPadButton.Down | OrbisPadButton.Left,
|
||||
6 => OrbisPadButton.Left,
|
||||
7 => OrbisPadButton.Left | OrbisPadButton.Up,
|
||||
_ => 0,
|
||||
};
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user