mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 452529985f | |||
| 5bb91eff9b | |||
| f4df0ed4bd | |||
| 23691a2bdc | |||
| 5c5ed8c064 | |||
| a4748ce266 | |||
| 5617646c8a | |||
| a4f3d3cd7f | |||
| 92526ecacf |
+1
-1
@@ -14,7 +14,7 @@ indent_size = 4
|
|||||||
indent_style = space
|
indent_style = space
|
||||||
tab_width = 4
|
tab_width = 4
|
||||||
|
|
||||||
[*.{md,json,yml,xml,props,csproj}]
|
[*.{md,json,yml,props,csproj}]
|
||||||
|
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
tab_width = 2
|
tab_width = 2
|
||||||
|
|||||||
@@ -16,11 +16,6 @@ body:
|
|||||||
Example title:
|
Example title:
|
||||||
Demon’s Souls (PPSA01341)
|
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
|
- type: input
|
||||||
id: game_name
|
id: game_name
|
||||||
attributes:
|
attributes:
|
||||||
@@ -78,20 +73,11 @@ body:
|
|||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Log File
|
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:
|
validations:
|
||||||
required: true
|
required: false
|
||||||
accept: ".log,.txt,.zip"
|
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
|
- type: dropdown
|
||||||
id: operating_system
|
id: operating_system
|
||||||
attributes:
|
attributes:
|
||||||
@@ -133,8 +119,7 @@ body:
|
|||||||
id: emulator_version
|
id: emulator_version
|
||||||
attributes:
|
attributes:
|
||||||
label: Emulator Version / Commit
|
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. v0.0.1 / a1b2c3d
|
||||||
placeholder: e.g. a1b2c3d
|
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 101 KiB After Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -26,7 +26,6 @@ arm64/
|
|||||||
*.userosscache
|
*.userosscache
|
||||||
*.sln.docstates
|
*.sln.docstates
|
||||||
|
|
||||||
.packages
|
|
||||||
packages/
|
packages/
|
||||||
*.nupkg
|
*.nupkg
|
||||||
.nuget/
|
.nuget/
|
||||||
@@ -37,6 +36,3 @@ packages/
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
Desktop.ini
|
Desktop.ini
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
|
|
||||||
.vs/
|
|
||||||
.idea/
|
|
||||||
|
|||||||
@@ -32,18 +32,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<DebugType>none</DebugType>
|
<DebugType>none</DebugType>
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
</PropertyGroup>
|
</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>
|
</Project>
|
||||||
|
|||||||
@@ -1,22 +1,16 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright (C) 2026 SharpEmu Emulator Project
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
-->
|
-->
|
||||||
|
|
||||||
<Project>
|
<Project>
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageVersion Include="Avalonia" Version="11.3.18" />
|
|
||||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
|
|
||||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
|
||||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
|
||||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
|
||||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||||
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
|
||||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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
-9
@@ -3,21 +3,13 @@ version = 1
|
|||||||
[[annotations]]
|
[[annotations]]
|
||||||
path = [
|
path = [
|
||||||
"REUSE.toml",
|
"REUSE.toml",
|
||||||
"nuget.config",
|
|
||||||
"global.json",
|
"global.json",
|
||||||
"**/packages.lock.json",
|
"**/packages.lock.json",
|
||||||
"scripts/ps5_names.txt",
|
"scripts/ps5_names.txt",
|
||||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||||
"_logs/**",
|
"_logs/**",
|
||||||
".github/images/**",
|
".github/images/**"
|
||||||
"assets/images/**"
|
|
||||||
]
|
]
|
||||||
precedence = "aggregate"
|
precedence = "aggregate"
|
||||||
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||||
SPDX-License-Identifier = "GPL-2.0-or-later"
|
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/">
|
<Folder Name="/src/">
|
||||||
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
|
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
|
||||||
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
|
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
|
||||||
<Project Path="src/SharpEmu.GUI/SharpEmu.GUI.csproj" />
|
|
||||||
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
|
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
|
||||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 92 KiB |
@@ -1,18 +0,0 @@
|
|||||||
<?xml version="1.0" encoding="utf-8"?>
|
|
||||||
|
|
||||||
<configuration>
|
|
||||||
<config>
|
|
||||||
<add key="globalPackagesFolder" value=".packages" />
|
|
||||||
</config>
|
|
||||||
|
|
||||||
<packageSources>
|
|
||||||
<clear />
|
|
||||||
<add key="Nuget" value="https://api.nuget.org/v3/index.json" />
|
|
||||||
</packageSources>
|
|
||||||
|
|
||||||
<packageSourceMapping>
|
|
||||||
<packageSource key="Nuget">
|
|
||||||
<package pattern="*" />
|
|
||||||
</packageSource>
|
|
||||||
</packageSourceMapping>
|
|
||||||
</configuration>
|
|
||||||
+2
-160
@@ -3,7 +3,6 @@
|
|||||||
|
|
||||||
using SharpEmu.Core.Runtime;
|
using SharpEmu.Core.Runtime;
|
||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
using SharpEmu.GUI;
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
@@ -25,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_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
|
||||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
||||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||||
private const int ATTACH_PARENT_PROCESS = -1;
|
|
||||||
private const int STD_OUTPUT_HANDLE = -11;
|
|
||||||
private const int STD_ERROR_HANDLE = -12;
|
|
||||||
private const uint GENERIC_READ = 0x80000000;
|
|
||||||
private const uint GENERIC_WRITE = 0x40000000;
|
|
||||||
private const uint FILE_SHARE_READ = 0x00000001;
|
|
||||||
private const uint FILE_SHARE_WRITE = 0x00000002;
|
|
||||||
private const uint OPEN_EXISTING = 3;
|
|
||||||
|
|
||||||
[STAThread]
|
|
||||||
private static int Main(string[] args)
|
private static int Main(string[] args)
|
||||||
{
|
{
|
||||||
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");
|
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||||
|
|
||||||
|
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||||
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
||||||
{
|
{
|
||||||
return childExitCode;
|
return childExitCode;
|
||||||
@@ -77,11 +43,9 @@ internal static partial class Program
|
|||||||
|
|
||||||
SharpEmuLog.MinimumLevel = logLevel;
|
SharpEmuLog.MinimumLevel = logLevel;
|
||||||
|
|
||||||
Log.Info(BuildInfo.Banner);
|
|
||||||
|
|
||||||
ebootPath = Path.GetFullPath(ebootPath);
|
ebootPath = Path.GetFullPath(ebootPath);
|
||||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||||
|
|
||||||
if (!File.Exists(ebootPath))
|
if (!File.Exists(ebootPath))
|
||||||
{
|
{
|
||||||
Log.Error($"EBOOT file was not found: {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;
|
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)
|
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
||||||
{
|
{
|
||||||
isMitigatedChild = false;
|
isMitigatedChild = false;
|
||||||
@@ -800,32 +670,4 @@ internal static partial class Program
|
|||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool CloseHandle(nint handle);
|
private static extern bool CloseHandle(nint handle);
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
|
||||||
private static extern nint GetConsoleWindow();
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool AttachConsole(int processId);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool AllocConsole();
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
private static extern nint GetStdHandle(int stdHandle);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool SetStdHandle(int stdHandle, nint handle);
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
|
|
||||||
private static extern nint CreateFileW(
|
|
||||||
string fileName,
|
|
||||||
uint desiredAccess,
|
|
||||||
uint shareMode,
|
|
||||||
nint securityAttributes,
|
|
||||||
uint creationDisposition,
|
|
||||||
uint flagsAndAttributes,
|
|
||||||
nint templateFile);
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,14 +7,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
|
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||||
<ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" />
|
|
||||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<!-- GUI subsystem so starting without arguments opens the frontend with no
|
<OutputType>Exe</OutputType>
|
||||||
console window; CLI mode re-attaches to the parent terminal's console. -->
|
|
||||||
<OutputType>WinExe</OutputType>
|
|
||||||
<AssemblyName>SharpEmu</AssemblyName>
|
<AssemblyName>SharpEmu</AssemblyName>
|
||||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
|
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
|
||||||
<SelfContained>true</SelfContained>
|
<SelfContained>true</SelfContained>
|
||||||
@@ -33,15 +30,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
|
|
||||||
<ApplicationIcon>..\..\assets\images\SharpEmu.ico</ApplicationIcon>
|
|
||||||
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<Content Include="..\..\LICENSE.txt">
|
<Content Include="..\..\LICENSE.txt">
|
||||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||||
@@ -50,17 +38,4 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</Content>
|
</Content>
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
|
||||||
library is embedded into the single-file bundle. -->
|
|
||||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
|
||||||
<ItemGroup>
|
|
||||||
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
|
|
||||||
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw'))" />
|
|
||||||
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
|
|
||||||
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
|
||||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
|
||||||
</ResolvedFileToPublish>
|
|
||||||
</ItemGroup>
|
|
||||||
</Target>
|
|
||||||
|
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -8,105 +8,6 @@
|
|||||||
"resolved": "10.0.3",
|
"resolved": "10.0.3",
|
||||||
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||||
},
|
},
|
||||||
"Avalonia.Angle.Windows.Natives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.1.25547.20250602",
|
|
||||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
|
||||||
},
|
|
||||||
"Avalonia.BuildServices": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.2",
|
|
||||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
|
||||||
},
|
|
||||||
"Avalonia.FreeDesktop": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18",
|
|
||||||
"Tmds.DBus.Protocol": "0.21.3"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Avalonia.Native": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Avalonia.Remote.Protocol": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
|
||||||
},
|
|
||||||
"Avalonia.Skia": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18",
|
|
||||||
"HarfBuzzSharp": "8.3.1.1",
|
|
||||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
|
||||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
|
||||||
"SkiaSharp": "2.88.9",
|
|
||||||
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
|
||||||
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Avalonia.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18",
|
|
||||||
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Avalonia.X11": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18",
|
|
||||||
"Avalonia.FreeDesktop": "11.3.18",
|
|
||||||
"Avalonia.Skia": "11.3.18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
|
||||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
|
||||||
},
|
|
||||||
"MicroCom.Runtime": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "0.11.0",
|
|
||||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
|
||||||
},
|
|
||||||
"Microsoft.DotNet.PlatformAbstractions": {
|
"Microsoft.DotNet.PlatformAbstractions": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.1.6",
|
"resolved": "3.1.6",
|
||||||
@@ -158,38 +59,6 @@
|
|||||||
"Silk.NET.Windowing.Common": "2.23.0"
|
"Silk.NET.Windowing.Common": "2.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"SkiaSharp": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
|
||||||
"dependencies": {
|
|
||||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
|
||||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.Linux": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
|
||||||
"dependencies": {
|
|
||||||
"SkiaSharp": "2.88.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.macOS": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
|
||||||
},
|
|
||||||
"Ultz.Native.GLFW": {
|
"Ultz.Native.GLFW": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.4.0",
|
"resolved": "3.4.0",
|
||||||
@@ -204,29 +73,14 @@
|
|||||||
"SharpEmu.Logging": "[1.0.0, )"
|
"SharpEmu.Logging": "[1.0.0, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sharpemu.gui": {
|
|
||||||
"type": "Project",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "[11.3.18, )",
|
|
||||||
"Avalonia.Desktop": "[11.3.18, )",
|
|
||||||
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
|
||||||
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
|
||||||
"SharpEmu.Logging": "[1.0.0, )",
|
|
||||||
"Tmds.DBus.Protocol": "[0.21.3, )"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"sharpemu.hle": {
|
"sharpemu.hle": {
|
||||||
"type": "Project",
|
"type": "Project"
|
||||||
"dependencies": {
|
|
||||||
"SharpEmu.Logging": "[1.0.0, )"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"sharpemu.libs": {
|
"sharpemu.libs": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SharpEmu.HLE": "[1.0.0, )",
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
"Silk.NET.Windowing": "[2.23.0, )"
|
"Silk.NET.Windowing": "[2.23.0, )"
|
||||||
}
|
}
|
||||||
@@ -234,48 +88,6 @@
|
|||||||
"sharpemu.logging": {
|
"sharpemu.logging": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
},
|
},
|
||||||
"Avalonia": {
|
|
||||||
"type": "CentralTransitive",
|
|
||||||
"requested": "[11.3.18, )",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia.BuildServices": "11.3.2",
|
|
||||||
"Avalonia.Remote.Protocol": "11.3.18",
|
|
||||||
"MicroCom.Runtime": "0.11.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Avalonia.Desktop": {
|
|
||||||
"type": "CentralTransitive",
|
|
||||||
"requested": "[11.3.18, )",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18",
|
|
||||||
"Avalonia.Native": "11.3.18",
|
|
||||||
"Avalonia.Skia": "11.3.18",
|
|
||||||
"Avalonia.Win32": "11.3.18",
|
|
||||||
"Avalonia.X11": "11.3.18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Avalonia.Fonts.Inter": {
|
|
||||||
"type": "CentralTransitive",
|
|
||||||
"requested": "[11.3.18, )",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Avalonia.Themes.Fluent": {
|
|
||||||
"type": "CentralTransitive",
|
|
||||||
"requested": "[11.3.18, )",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Iced": {
|
"Iced": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[1.21.0, )",
|
"requested": "[1.21.0, )",
|
||||||
@@ -291,16 +103,6 @@
|
|||||||
"Silk.NET.Core": "2.23.0"
|
"Silk.NET.Core": "2.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
|
||||||
"type": "CentralTransitive",
|
|
||||||
"requested": "[2.23.0, )",
|
|
||||||
"resolved": "2.23.0",
|
|
||||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Silk.NET.Core": "2.23.0",
|
|
||||||
"Silk.NET.Vulkan": "2.23.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
@@ -320,61 +122,9 @@
|
|||||||
"Silk.NET.Windowing.Common": "2.23.0",
|
"Silk.NET.Windowing.Common": "2.23.0",
|
||||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"Tmds.DBus.Protocol": {
|
|
||||||
"type": "CentralTransitive",
|
|
||||||
"requested": "[0.21.3, )",
|
|
||||||
"resolved": "0.21.3",
|
|
||||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net10.0/linux-x64": {
|
"net10.0/linux-x64": {
|
||||||
"Avalonia.Angle.Windows.Natives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.1.25547.20250602",
|
|
||||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
|
||||||
},
|
|
||||||
"Avalonia.Native": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.Linux": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
|
||||||
"dependencies": {
|
|
||||||
"SkiaSharp": "2.88.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.macOS": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
|
||||||
},
|
|
||||||
"Ultz.Native.GLFW": {
|
"Ultz.Native.GLFW": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.4.0",
|
"resolved": "3.4.0",
|
||||||
@@ -382,52 +132,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net10.0/osx-arm64": {
|
"net10.0/osx-arm64": {
|
||||||
"Avalonia.Angle.Windows.Natives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.1.25547.20250602",
|
|
||||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
|
||||||
},
|
|
||||||
"Avalonia.Native": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.Linux": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
|
||||||
"dependencies": {
|
|
||||||
"SkiaSharp": "2.88.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.macOS": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
|
||||||
},
|
|
||||||
"Ultz.Native.GLFW": {
|
"Ultz.Native.GLFW": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.4.0",
|
"resolved": "3.4.0",
|
||||||
@@ -435,52 +139,6 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"net10.0/win-x64": {
|
"net10.0/win-x64": {
|
||||||
"Avalonia.Angle.Windows.Natives": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.1.25547.20250602",
|
|
||||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
|
||||||
},
|
|
||||||
"Avalonia.Native": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "11.3.18",
|
|
||||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
|
||||||
"dependencies": {
|
|
||||||
"Avalonia": "11.3.18"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
|
||||||
},
|
|
||||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "8.3.1.1",
|
|
||||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.Linux": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
|
||||||
"dependencies": {
|
|
||||||
"SkiaSharp": "2.88.9"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.macOS": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
|
||||||
},
|
|
||||||
"SkiaSharp.NativeAssets.Win32": {
|
|
||||||
"type": "Transitive",
|
|
||||||
"resolved": "2.88.9",
|
|
||||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
|
||||||
},
|
|
||||||
"Ultz.Native.GLFW": {
|
"Ultz.Native.GLFW": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "3.4.0",
|
"resolved": "3.4.0",
|
||||||
|
|||||||
@@ -7,14 +7,11 @@ using SharpEmu.Core.Cpu.Native;
|
|||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu;
|
namespace SharpEmu.Core.Cpu;
|
||||||
|
|
||||||
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
|
|
||||||
|
|
||||||
private enum EntryFrameKind
|
private enum EntryFrameKind
|
||||||
{
|
{
|
||||||
ProcessEntry,
|
ProcessEntry,
|
||||||
@@ -83,8 +80,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string processImageName = "eboot.bin",
|
string processImageName = "eboot.bin",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Log.Debug("=== DispatchEntry START ===");
|
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
||||||
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -92,7 +89,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
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;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -105,8 +103,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string moduleName = "module",
|
string moduleName = "module",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Log.Debug("=== DispatchModuleInitializer START ===");
|
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
||||||
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -121,7 +119,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
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;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -135,7 +134,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
CpuExecutionOptions executionOptions = default,
|
CpuExecutionOptions executionOptions = default,
|
||||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||||
{
|
{
|
||||||
Log.Debug("DispatchEntryCore STARTING...");
|
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
||||||
|
|
||||||
LastEntryPoint = entryPoint;
|
LastEntryPoint = entryPoint;
|
||||||
LastTrapInfo = null;
|
LastTrapInfo = null;
|
||||||
@@ -307,7 +306,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
LastMilestoneLog,
|
LastMilestoneLog,
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
$"CpuEngine native-only failed: {backendError}");
|
$"CpuEngine native-only failed: {backendError}");
|
||||||
Log.Error($"Native backend FAILED: {backendError}");
|
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
||||||
return FailEarly(
|
return FailEarly(
|
||||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||||
CpuExitReason.NativeBackendUnavailable);
|
CpuExitReason.NativeBackendUnavailable);
|
||||||
@@ -416,8 +415,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
|
|
||||||
const ulong entryParamsSize = 0x20;
|
const ulong entryParamsSize = 0x20;
|
||||||
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
||||||
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
|
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
|
||||||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
|
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
||||||
@@ -457,6 +456,13 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
return value & ~(alignment - 1);
|
return value & ~(alignment - 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return context.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildEntryFrameDiagnostic(
|
private static string BuildEntryFrameDiagnostic(
|
||||||
ulong entryPoint,
|
ulong entryPoint,
|
||||||
CpuContext context,
|
CpuContext context,
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -44,7 +43,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
|
Console.Error.WriteLine($"[LOADER][INFO] Recent import calls ({_recentImportTraceCount}):");
|
||||||
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
|
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
|
||||||
for (int i = 0; i < _recentImportTraceCount; i++)
|
for (int i = 0; i < _recentImportTraceCount; i++)
|
||||||
{
|
{
|
||||||
@@ -52,8 +51,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var entry = _recentImportTrace[num2];
|
var entry = _recentImportTrace[num2];
|
||||||
if (!string.IsNullOrEmpty(entry.Nid))
|
if (!string.IsNullOrEmpty(entry.Nid))
|
||||||
{
|
{
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$" #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
$"[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}");
|
$"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);
|
list.Add(num7);
|
||||||
if (num2 < 32)
|
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++;
|
num2++;
|
||||||
}
|
}
|
||||||
if (num >= 16384)
|
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;
|
return list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,7 +106,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
if (num != 0)
|
if (num != 0)
|
||||||
{
|
{
|
||||||
Log.Warning($"Suspicious unresolved pointer hits: {num}");
|
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer hits: {num}");
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@@ -122,18 +121,18 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Span<byte> destination = stackalloc byte[128];
|
Span<byte> destination = stackalloc byte[128];
|
||||||
if (!cpuContext.Memory.TryRead(returnRip, destination))
|
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;
|
return;
|
||||||
}
|
}
|
||||||
string value = BitConverter.ToString(destination.ToArray()).Replace("-", " ");
|
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))
|
if (destination[0] == byte.MaxValue && (destination[1] == 21 || destination[1] == 37))
|
||||||
{
|
{
|
||||||
int num = BitConverter.ToInt32(destination.Slice(2, 4));
|
int num = BitConverter.ToInt32(destination.Slice(2, 4));
|
||||||
ulong num2 = returnRip + 6 + (ulong)num;
|
ulong num2 = returnRip + 6 + (ulong)num;
|
||||||
if (cpuContext.TryReadUInt64(num2, out var value2))
|
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)
|
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;
|
ulong num4 = returnRip + 7 + (ulong)num3;
|
||||||
if (cpuContext.TryReadUInt64(num4, out var value3))
|
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++)
|
for (int i = 0; i + 6 <= destination.Length; i++)
|
||||||
@@ -154,7 +153,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong num7 = num6 + 6 + (ulong)num5;
|
ulong num7 = num6 + 6 + (ulong)num5;
|
||||||
if (cpuContext.TryReadUInt64(num7, out var value4))
|
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));
|
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
|
||||||
ulong callRip = returnRip + (ulong)i;
|
ulong callRip = returnRip + (ulong)i;
|
||||||
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
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++)
|
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||||
{
|
{
|
||||||
if (_importEntries[importIndex].Address != target)
|
if (_importEntries[importIndex].Address != target)
|
||||||
@@ -180,21 +179,21 @@ public sealed partial class DirectExecutionBackend
|
|||||||
string nid = _importEntries[importIndex].Nid;
|
string nid = _importEntries[importIndex].Nid;
|
||||||
if (_moduleManager.TryGetExport(nid, out var export))
|
if (_moduleManager.TryGetExport(nid, out var export))
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cpuContext.Memory.TryRead(target, targetBytes))
|
if (cpuContext.Memory.TryRead(target, targetBytes))
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
||||||
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
|
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
|
||||||
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
|
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
|
||||||
{
|
{
|
||||||
@@ -202,8 +201,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
|
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
|
||||||
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
|
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||||
{
|
{
|
||||||
if (_importEntries[importIndex].Address != slotTarget)
|
if (_importEntries[importIndex].Address != slotTarget)
|
||||||
@@ -214,13 +213,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
string nid = _importEntries[importIndex].Nid;
|
string nid = _importEntries[importIndex].Nid;
|
||||||
if (_moduleManager.TryGetExport(nid, out var export))
|
if (_moduleManager.TryGetExport(nid, out var export))
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,15 +103,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
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))
|
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -19,16 +19,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
|
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
|
try
|
||||||
{
|
{
|
||||||
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
|
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}");
|
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
|
||||||
return 18446744071562199298uL;
|
return 18446744071562199298uL;
|
||||||
}
|
}
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (LogThreadMode)
|
|
||||||
{
|
|
||||||
_threadModeGatewayDepth--;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
|
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
|
||||||
@@ -86,12 +69,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
WriteCtxU64(contextRecord, 152, nextRsp);
|
WriteCtxU64(contextRecord, 152, nextRsp);
|
||||||
WriteCtxU64(contextRecord, 248, returnRip);
|
WriteCtxU64(contextRecord, 248, returnRip);
|
||||||
Interlocked.Increment(ref _rawSentinelRecoveries);
|
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 -1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -122,12 +99,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
|
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
|
||||||
_lastReportedRawSentinelRecoveries = num2;
|
_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) &&
|
if (IsLeafImport(importStubEntry.Nid) &&
|
||||||
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
|
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.R13] = *(ulong*)(argPackPtr + 72);
|
||||||
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
||||||
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
||||||
// The trampoline spills the SysV variadic XMM save area (xmm0..xmm7) into the
|
cpuContext.SetXmmRegister(
|
||||||
// 0x80 bytes immediately below the GP argpack, so variadic float args (printf
|
0,
|
||||||
// %f, and powf/logf inputs) reach the handler. xmm{i} is at argPackPtr-0x80+i*16.
|
*(ulong*)(argPackPtr - 16),
|
||||||
for (var xmmIndex = 0; xmmIndex < 8; xmmIndex++)
|
*(ulong*)(argPackPtr - 8));
|
||||||
{
|
|
||||||
var xmmSlot = argPackPtr - 0x80 + (xmmIndex * 16);
|
|
||||||
cpuContext.SetXmmRegister(
|
|
||||||
xmmIndex,
|
|
||||||
*(ulong*)xmmSlot,
|
|
||||||
*(ulong*)(xmmSlot + 8));
|
|
||||||
}
|
|
||||||
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
||||||
ulong value = cpuContext[CpuRegister.Rdi];
|
ulong value = cpuContext[CpuRegister.Rdi];
|
||||||
ulong value2 = cpuContext[CpuRegister.Rsi];
|
ulong value2 = cpuContext[CpuRegister.Rsi];
|
||||||
@@ -227,17 +191,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.Rax] = 1uL;
|
cpuContext[CpuRegister.Rax] = 1uL;
|
||||||
return 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 flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
||||||
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||||
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
||||||
@@ -385,15 +338,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
orbisGen2Result = DispatchBootstrapBridge();
|
orbisGen2Result = DispatchBootstrapBridge();
|
||||||
}
|
}
|
||||||
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
|
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal))
|
||||||
string.Equals(importStubEntry.Nid, "LwG8g3niqwA", StringComparison.Ordinal))
|
|
||||||
{
|
{
|
||||||
orbisGen2Result = DispatchKernelDynlibDlsym();
|
orbisGen2Result = DispatchKernelDynlibDlsym();
|
||||||
}
|
}
|
||||||
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
|
|
||||||
}
|
|
||||||
else if (importStubEntry.Export is { } cachedExport &&
|
else if (importStubEntry.Export is { } cachedExport &&
|
||||||
(cachedExport.Target & cpuContext.TargetGeneration) != 0)
|
(cachedExport.Target & cpuContext.TargetGeneration) != 0)
|
||||||
{
|
{
|
||||||
@@ -405,21 +353,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
orbisGen2Result = (OrbisGen2Result)returnValue;
|
orbisGen2Result = (OrbisGen2Result)returnValue;
|
||||||
}
|
}
|
||||||
else if (importStubEntry.Export is { } mismatchedExport)
|
else
|
||||||
{
|
{
|
||||||
dispatchResolved = true;
|
dispatchResolved = false;
|
||||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||||
Console.Error.WriteLine(
|
}
|
||||||
$"[LOADER][WARN] Import#{num} not implemented for generation {cpuContext.TargetGeneration}: " +
|
|
||||||
$"nid={importStubEntry.Nid} targets={mismatchedExport.Target} ret=0x{num7:X16}");
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
dispatchResolved = false;
|
|
||||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
||||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -538,13 +477,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.Flush();
|
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];
|
return cpuContext[CpuRegister.Rax];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -676,13 +608,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
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) =>
|
private static bool IsNoBlockLeafImport(string nid) =>
|
||||||
nid is
|
nid is
|
||||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||||
@@ -703,57 +628,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||||
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
||||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
"qvMUCyyaCSI"; // sceKernelAprSubmitCommandBufferAndGetId
|
||||||
"Q2V+iqvjgC0" or // vsnprintf
|
|
||||||
"AV6ipCNa4Rw" or // strcasecmp
|
|
||||||
"q1cHNfGycLI" or // scePadRead
|
|
||||||
"xk0AcarP3V4" or // scePadOpen
|
|
||||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
|
||||||
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
|
|
||||||
"K-jXhbt2gn4"; // pthread_mutex_trylock (scePthreadMutexTrylock is upoVrzMHFeE)
|
|
||||||
|
|
||||||
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
||||||
{
|
{
|
||||||
var resultValue = unchecked((int)result);
|
|
||||||
if (resultValue > 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var expectedFileProbeMiss =
|
var expectedFileProbeMiss =
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND &&
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND &&
|
||||||
IsExpectedFileProbeNotFoundNid(nid);
|
IsExpectedFileProbeNotFoundNid(nid);
|
||||||
var expectedTimedWaitTimeout =
|
var expectedTimedWaitTimeout =
|
||||||
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
|
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
|
||||||
unchecked((int)result) == 60;
|
unchecked((int)result) == 60;
|
||||||
var expectedEqueueTimeout =
|
|
||||||
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
|
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
|
||||||
var expectedMutexTrylockBusy =
|
var expectedMutexTrylockBusy =
|
||||||
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||||
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
var expectedUserServiceNoEvent =
|
if (!expectedFileProbeMiss && !expectedTimedWaitTimeout && !expectedMutexTrylockBusy)
|
||||||
string.Equals(nid, "yH17Q6NWtVg", StringComparison.Ordinal) &&
|
|
||||||
resultValue == unchecked((int)0x80960007);
|
|
||||||
var expectedPrivacyInvalidParameter =
|
|
||||||
string.Equals(nid, "D-CzAxQL0XI", StringComparison.Ordinal) &&
|
|
||||||
resultValue == unchecked((int)0x80960009);
|
|
||||||
if (!expectedFileProbeMiss &&
|
|
||||||
!expectedTimedWaitTimeout &&
|
|
||||||
!expectedEqueueTimeout &&
|
|
||||||
!expectedMutexTrylockBusy &&
|
|
||||||
!expectedUserServiceNoEvent &&
|
|
||||||
!expectedPrivacyInvalidParameter)
|
|
||||||
{
|
{
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ShouldLogExpectedImportResults())
|
var key = nid + "\0" + (int)result;
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var key = nid + "\0" + resultValue;
|
|
||||||
int count;
|
int count;
|
||||||
lock (_importResultLogSampleGate)
|
lock (_importResultLogSampleGate)
|
||||||
{
|
{
|
||||||
@@ -765,53 +658,28 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return count <= 8 || count % 10000 == 0;
|
return count <= 8 || count % 10000 == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ShouldLogExpectedImportResults() =>
|
|
||||||
string.Equals(
|
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EXPECTED_IMPORT_RESULTS"),
|
|
||||||
"1",
|
|
||||||
StringComparison.Ordinal);
|
|
||||||
|
|
||||||
private static bool IsExpectedFileProbeNotFoundNid(string nid) =>
|
private static bool IsExpectedFileProbeNotFoundNid(string nid) =>
|
||||||
nid is
|
nid is
|
||||||
"eV9wAD2riIA" or // sceKernelStat
|
"eV9wAD2riIA" or // sceKernelStat
|
||||||
"1G3lF1Gg1k8" or // sceKernelOpen
|
"1G3lF1Gg1k8" or // sceKernelOpen
|
||||||
"gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes
|
"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)
|
private bool IsLeafImport(string nid)
|
||||||
{
|
{
|
||||||
if (nid == "1jfXLRVzisc") // sceKernelUsleep
|
if (nid == "1jfXLRVzisc")
|
||||||
{
|
{
|
||||||
return !_logUsleep;
|
return !_logUsleep;
|
||||||
}
|
}
|
||||||
|
|
||||||
return nid is
|
return nid is
|
||||||
"9UK1vLZQft4" or // scePthreadMutexLock
|
"9UK1vLZQft4" or
|
||||||
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
"tn3VlD0hG60" or
|
||||||
"7H0iTOciTLo" or // pthread_mutex_lock
|
"7H0iTOciTLo" or
|
||||||
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
"2Z+PpY6CaJg" or
|
||||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
"8aI7R7WaOlc" or
|
||||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
"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
|
"LtTouSCZjHM" or // sceAgcCbNop
|
||||||
"k3GhuSNmBLU" or // sceAgcCbDispatch
|
"k3GhuSNmBLU" or // sceAgcCbDispatch
|
||||||
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
|
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
|
||||||
@@ -834,24 +702,24 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
|
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
|
||||||
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
|
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
|
||||||
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
|
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
|
||||||
"a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
|
"a8uLzYY--tM" or
|
||||||
"Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
|
"Qs1xtplKo0U" or
|
||||||
"GuchCTefuZw" or // sceAmprCommandBufferDestructor
|
"GuchCTefuZw" or
|
||||||
"N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
|
"N-FSPA4S3nI" or
|
||||||
"baQO9ez2gL4" or // sceAmprCommandBufferReset
|
"baQO9ez2gL4" or
|
||||||
"ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
|
"ULvXMDz56po" or
|
||||||
"mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
|
"mQ16-QdKv7k" or
|
||||||
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
|
"vWU-odnS+fU" or
|
||||||
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
|
"sSAUCCU1dv4" or
|
||||||
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
|
"C+IEj+BsAFM" or
|
||||||
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
|
"tZDDEo2tE5k" or
|
||||||
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
|
"GnxKOHEawhk" or
|
||||||
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
|
"H896Pt-yB4I" or
|
||||||
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
|
"sJXyWHjP-F8" or
|
||||||
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
"ASoW5WE-UPo" or
|
||||||
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
"rqwFKI4PAiM" or
|
||||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
"eE4Szl8sil8" or
|
||||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
"qvMUCyyaCSI" or
|
||||||
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
|
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
|
||||||
"TywrFKCoLGY" or // sceSaveDataInitialize3
|
"TywrFKCoLGY" or // sceSaveDataInitialize3
|
||||||
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
|
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
|
||||||
@@ -870,7 +738,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"WkkeywLJcgU" or // wcslen
|
"WkkeywLJcgU" or // wcslen
|
||||||
"Ovb2dSJOAuE" or // strcmp
|
"Ovb2dSJOAuE" or // strcmp
|
||||||
"aesyjrHVWy4" or // strncmp
|
"aesyjrHVWy4" or // strncmp
|
||||||
"AV6ipCNa4Rw" or // strcasecmp
|
|
||||||
"pNtJdE3x49E" or // wcscmp
|
"pNtJdE3x49E" or // wcscmp
|
||||||
"fV2xHER+bKE" or // wcscoll
|
"fV2xHER+bKE" or // wcscoll
|
||||||
"E8wCoUEbfzk" or // wcsncmp
|
"E8wCoUEbfzk" or // wcsncmp
|
||||||
@@ -896,12 +763,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"vz+pg2zdopI" or // sceKernelGetEventUserData
|
"vz+pg2zdopI" or // sceKernelGetEventUserData
|
||||||
"mJ7aghmgvfc" or // sceKernelGetEventId
|
"mJ7aghmgvfc" or // sceKernelGetEventId
|
||||||
"23CPPI1tyBY" or // sceKernelGetEventFilter
|
"23CPPI1tyBY" or // sceKernelGetEventFilter
|
||||||
"kwGyyjohI50" or // sceKernelGetEventData
|
"kwGyyjohI50"; // 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
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private long NextImportDispatchIndex()
|
private long NextImportDispatchIndex()
|
||||||
@@ -1318,11 +1180,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
|
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress))
|
||||||
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'");
|
|
||||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -1335,62 +1194,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address)
|
|
||||||
{
|
|
||||||
address = 0;
|
|
||||||
var alias = symbolName switch
|
|
||||||
{
|
|
||||||
"scriptingGetMem" => "malloc",
|
|
||||||
"scriptingFreeMem" => "free",
|
|
||||||
"scriptingRealloc" => "realloc",
|
|
||||||
"scriptingCalloc" => "calloc",
|
|
||||||
_ => null,
|
|
||||||
};
|
|
||||||
|
|
||||||
return alias != null && TryResolveRuntimeSymbolAddress(alias, out address);
|
|
||||||
}
|
|
||||||
|
|
||||||
private OrbisGen2Result DispatchIl2CppApiLookupSymbol()
|
|
||||||
{
|
|
||||||
var cpuContext = ActiveCpuContext;
|
|
||||||
if (cpuContext == null)
|
|
||||||
{
|
|
||||||
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var symbolNameAddress = cpuContext[CpuRegister.Rdi];
|
|
||||||
var outputAddress = cpuContext[CpuRegister.Rsi];
|
|
||||||
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
|
|
||||||
outputAddress == 0 ||
|
|
||||||
!TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress) ||
|
|
||||||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}' out=0x{outputAddress:X16}");
|
|
||||||
if (outputAddress != 0)
|
|
||||||
{
|
|
||||||
_ = TryWriteUInt64Compat(outputAddress, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
|
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
cpuContext[CpuRegister.Rax] = 0;
|
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address)
|
|
||||||
{
|
|
||||||
if (TryResolveRuntimeSymbolAddress(symbolName, out address))
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
return Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) &&
|
|
||||||
TryResolveRuntimeSymbolAddress(symbol.Nid, out address);
|
|
||||||
}
|
|
||||||
|
|
||||||
private OrbisGen2Result DispatchBootstrapBridge()
|
private OrbisGen2Result DispatchBootstrapBridge()
|
||||||
{
|
{
|
||||||
var cpuContext = ActiveCpuContext;
|
var cpuContext = ActiveCpuContext;
|
||||||
|
|||||||
@@ -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.Loader;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, IGuestThreadScheduler, IDisposable
|
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 ImportLoopHistoryLength = 2048;
|
||||||
|
|
||||||
private const int ImportLoopWideDiversityWindow = 768;
|
private const int ImportLoopWideDiversityWindow = 768;
|
||||||
@@ -424,10 +422,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
private void ThreadMain()
|
private void ThreadMain()
|
||||||
{
|
{
|
||||||
var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_guestThreadHandle);
|
var previousGuestThreadHandle = GuestThreadExecution.EnterGuestThread(_guestThreadHandle);
|
||||||
if (LogThreadMode)
|
|
||||||
{
|
|
||||||
TraceThreadMode($"runner_start guest=0x{_guestThreadHandle:X16}");
|
|
||||||
}
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (true)
|
while (true)
|
||||||
@@ -438,11 +432,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (LogThreadMode)
|
|
||||||
{
|
|
||||||
_threadModeCycleId = Interlocked.Increment(ref _threadModeCycleCounter);
|
|
||||||
TraceThreadMode($"runner_run guest=0x{_guestThreadHandle:X16}");
|
|
||||||
}
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
_work?.Invoke();
|
_work?.Invoke();
|
||||||
@@ -455,10 +444,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
if (LogThreadMode)
|
|
||||||
{
|
|
||||||
TraceThreadMode($"runner_stop guest=0x{_guestThreadHandle:X16}");
|
|
||||||
}
|
|
||||||
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
|
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -482,14 +467,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private readonly Queue<GuestThreadState> _readyGuestThreads = new Queue<GuestThreadState>();
|
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 int _readyGuestThreadCount;
|
||||||
|
|
||||||
private readonly Dictionary<ulong, GuestThreadState> _guestThreads = new Dictionary<ulong, GuestThreadState>();
|
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;
|
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_COMMIT = 4096u;
|
||||||
|
|
||||||
private const uint MEM_RESERVE = 8192u;
|
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)
|
private unsafe static int CallNativeEntry(void* entry)
|
||||||
{
|
{
|
||||||
var nativeEntry = (delegate* unmanaged[Cdecl]<int>)entry;
|
var nativeEntry = (delegate* unmanaged[Cdecl]<int>)entry;
|
||||||
if (!LogThreadMode)
|
return nativeEntry();
|
||||||
{
|
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe static void WriteCtxU64(void* contextRecord, int offset, ulong value)
|
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();
|
_importLoopGuardSeconds = GetImportLoopGuardSeconds();
|
||||||
_entryReturnSentinelRip = 0uL;
|
_entryReturnSentinelRip = 0uL;
|
||||||
_forcedGuestExit = false;
|
_forcedGuestExit = false;
|
||||||
_guestTeardownRequested = false;
|
|
||||||
_importLoopSignatureCount = 0;
|
_importLoopSignatureCount = 0;
|
||||||
_importLoopSignatureWriteIndex = 0;
|
_importLoopSignatureWriteIndex = 0;
|
||||||
_importLoopPatternHits = 0;
|
_importLoopPatternHits = 0;
|
||||||
@@ -1181,72 +1120,21 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
0x75, 0xF5,
|
0x75, 0xF5,
|
||||||
0xC3,
|
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" =>
|
"Q3VBxCXhUHs" =>
|
||||||
[
|
[
|
||||||
0x48, 0x89, 0xF8, // mov rax, rdi (return dst)
|
0x48, 0x89, 0xF8,
|
||||||
0x48, 0x81, 0xFF, 0x00, 0x00, 0x01, 0x00, // cmp rdi, 0x10000
|
0x48, 0x89, 0xD1,
|
||||||
0x72, 0x31, // jb done
|
0xF3, 0xA4,
|
||||||
0x49, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, // mov r8, 0x800000000000
|
0xC3,
|
||||||
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
|
|
||||||
],
|
],
|
||||||
"8zTFvBIAIN8" =>
|
"8zTFvBIAIN8" =>
|
||||||
[
|
[
|
||||||
0x48, 0x89, 0xF8, // mov rax, rdi (return dst)
|
0x49, 0x89, 0xF8,
|
||||||
0x48, 0x81, 0xFF, 0x00, 0x00, 0x01, 0x00, // cmp rdi, 0x10000
|
0x48, 0x89, 0xF0,
|
||||||
0x72, 0x2B, // jb done
|
0x48, 0x89, 0xD1,
|
||||||
0x49, 0xB8, 0x00, 0x00, 0x00, 0x00, 0x00, 0x80, 0x00, 0x00, // mov r8, 0x800000000000
|
0xF3, 0xAA,
|
||||||
0x4C, 0x39, 0xC7, // cmp rdi, r8
|
0x4C, 0x89, 0xC0,
|
||||||
0x73, 0x1C, // jae done
|
0xC3,
|
||||||
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
|
|
||||||
],
|
],
|
||||||
_ => default,
|
_ => default,
|
||||||
};
|
};
|
||||||
@@ -1465,25 +1353,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
{
|
{
|
||||||
return exportName switch
|
return exportName switch
|
||||||
{
|
{
|
||||||
|
"memcpy" or
|
||||||
"memmove" or
|
"memmove" or
|
||||||
// memset/memcpy excluded: the raw LLE routines have no null/bounds guard and crash
|
"memset" or
|
||||||
// with an access violation on bad pointers (observed hit during Quake's CL_Init,
|
"memcmp" => true,
|
||||||
// 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,
|
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1695,7 +1568,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
private unsafe nint CreateImportHandlerTrampoline(int importIndex)
|
private unsafe nint CreateImportHandlerTrampoline(int importIndex)
|
||||||
{
|
{
|
||||||
void* ptr = VirtualAlloc(null, 256u, 12288u, 64u);
|
void* ptr = VirtualAlloc(null, 192u, 12288u, 64u);
|
||||||
if (ptr == null)
|
if (ptr == null)
|
||||||
{
|
{
|
||||||
return 0;
|
return 0;
|
||||||
@@ -1723,21 +1596,20 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ptr2[num++] = 82;
|
ptr2[num++] = 82;
|
||||||
ptr2[num++] = 86;
|
ptr2[num++] = 86;
|
||||||
ptr2[num++] = 87;
|
ptr2[num++] = 87;
|
||||||
// sub rsp, 0x80 — reserve 8*16 bytes for the SysV variadic XMM save area
|
ptr2[num++] = 72;
|
||||||
ptr2[num++] = 0x48; ptr2[num++] = 0x81; ptr2[num++] = 0xEC;
|
ptr2[num++] = 131;
|
||||||
ptr2[num++] = 0x80; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 0x00;
|
ptr2[num++] = 236;
|
||||||
// movdqu [rsp + i*0x10], xmm{i} for i = 0..7 (F3 0F 7F /r, SIB=0x24 base=rsp, disp8)
|
ptr2[num++] = 16;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x44; ptr2[num++] = 0x24; ptr2[num++] = 0x00; // xmm0
|
ptr2[num++] = 243;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x4C; ptr2[num++] = 0x24; ptr2[num++] = 0x10; // xmm1
|
ptr2[num++] = 15;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x54; ptr2[num++] = 0x24; ptr2[num++] = 0x20; // xmm2
|
ptr2[num++] = 127;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x5C; ptr2[num++] = 0x24; ptr2[num++] = 0x30; // xmm3
|
ptr2[num++] = 4;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x64; ptr2[num++] = 0x24; ptr2[num++] = 0x40; // xmm4
|
ptr2[num++] = 36;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x6C; ptr2[num++] = 0x24; ptr2[num++] = 0x50; // xmm5
|
ptr2[num++] = 76;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x74; ptr2[num++] = 0x24; ptr2[num++] = 0x60; // xmm6
|
ptr2[num++] = 141;
|
||||||
ptr2[num++] = 0xF3; ptr2[num++] = 0x0F; ptr2[num++] = 0x7F; ptr2[num++] = 0x7C; ptr2[num++] = 0x24; ptr2[num++] = 0x70; // xmm7
|
ptr2[num++] = 100;
|
||||||
// lea r12, [rsp + 0x80] — r12 = argpack base (the 12 pushed GP regs), past the XMM area
|
ptr2[num++] = 36;
|
||||||
ptr2[num++] = 0x4C; ptr2[num++] = 0x8D; ptr2[num++] = 0xA4; ptr2[num++] = 0x24;
|
ptr2[num++] = 16;
|
||||||
ptr2[num++] = 0x80; ptr2[num++] = 0x00; ptr2[num++] = 0x00; ptr2[num++] = 0x00;
|
|
||||||
ptr2[num++] = 72;
|
ptr2[num++] = 72;
|
||||||
ptr2[num++] = 131;
|
ptr2[num++] = 131;
|
||||||
ptr2[num++] = 236;
|
ptr2[num++] = 236;
|
||||||
@@ -1785,11 +1657,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ptr2[num++] = 131;
|
ptr2[num++] = 131;
|
||||||
ptr2[num++] = 196;
|
ptr2[num++] = 196;
|
||||||
ptr2[num++] = 40;
|
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++] = 76;
|
||||||
ptr2[num++] = 137;
|
ptr2[num++] = 137;
|
||||||
ptr2[num++] = 228;
|
ptr2[num++] = 228;
|
||||||
@@ -1812,14 +1679,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ptr2[num++] = 65;
|
ptr2[num++] = 65;
|
||||||
ptr2[num++] = 95;
|
ptr2[num++] = 95;
|
||||||
ptr2[num++] = 195;
|
ptr2[num++] = 195;
|
||||||
uint num2 = default(uint);
|
uint num2 = default(uint);
|
||||||
if (!VirtualProtect(ptr, 256u, 32u, &num2))
|
VirtualProtect(ptr, 192u, 32u, &num2);
|
||||||
{
|
FlushInstructionCache(GetCurrentProcess(), ptr, 192u);
|
||||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for import dispatch stub at 0x{(nint)ptr:X16}");
|
return (nint)ptr;
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, 256u);
|
|
||||||
return (nint)ptr;
|
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
{
|
{
|
||||||
@@ -1900,11 +1763,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
tlsHandlerAddress[num++] = 195;
|
tlsHandlerAddress[num++] = 195;
|
||||||
_tlsPatchStubOffset = (num + 15) & ~15;
|
_tlsPatchStubOffset = (num + 15) & ~15;
|
||||||
uint num2 = default(uint);
|
uint num2 = default(uint);
|
||||||
if (!VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2))
|
VirtualProtect((void*)_tlsHandlerAddress, TlsHandlerRegionSize, 32u, &num2);
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS handler at 0x{_tlsHandlerAddress:X16}");
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, TlsHandlerRegionSize);
|
FlushInstructionCache(GetCurrentProcess(), (void*)_tlsHandlerAddress, TlsHandlerRegionSize);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] TLS handler at 0x{_tlsHandlerAddress:X16}");
|
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;
|
ptr2[i] = 144;
|
||||||
}
|
}
|
||||||
uint num = default(uint);
|
uint num = default(uint);
|
||||||
if (!VirtualProtect(ptr, 4096u, 32u, &num))
|
VirtualProtect(ptr, 4096u, 32u, &num);
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for unresolved return stub at 0x{(nint)ptr:X16}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, 16u);
|
FlushInstructionCache(GetCurrentProcess(), ptr, 16u);
|
||||||
return (nint)ptr;
|
return (nint)ptr;
|
||||||
}
|
}
|
||||||
@@ -1976,11 +1831,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
EmitByte(code, ref offset, 0xC3);
|
EmitByte(code, ref offset, 0xC3);
|
||||||
|
|
||||||
uint oldProtect = default;
|
uint oldProtect = default;
|
||||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for guest return stub at 0x{(nint)ptr:X16}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||||
return (nint)ptr;
|
return (nint)ptr;
|
||||||
}
|
}
|
||||||
@@ -1996,36 +1847,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
byte* code = (byte*)ptr;
|
byte* code = (byte*)ptr;
|
||||||
int offset = 0;
|
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, 0x54); // push r12
|
||||||
EmitByte(code, ref offset, 0x41); EmitByte(code, ref offset, 0x55); // push r13
|
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
|
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));
|
*(int*)(code + guestRestoreJump) = restoreOffset - (guestRestoreJump + sizeof(int));
|
||||||
|
|
||||||
uint oldProtect = default;
|
uint oldProtect = default;
|
||||||
if (!VirtualProtect(ptr, stubSize, 32u, &oldProtect))
|
VirtualProtect(ptr, stubSize, 32u, &oldProtect);
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for exception handler trampoline at 0x{(nint)ptr:X16}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
FlushInstructionCache(GetCurrentProcess(), ptr, (nuint)offset);
|
||||||
return (nint)ptr;
|
return (nint)ptr;
|
||||||
}
|
}
|
||||||
@@ -2437,11 +2254,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ptr[num2++] = 144;
|
ptr[num2++] = 144;
|
||||||
}
|
}
|
||||||
uint flNewProtect = default(uint);
|
uint flNewProtect = default(uint);
|
||||||
if (!VirtualProtect((void*)num, 32u, 32u, &flNewProtect))
|
VirtualProtect((void*)num, 32u, 32u, &flNewProtect);
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][ERROR] VirtualProtect failed for TLS store helper at 0x{num:X16}");
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
FlushInstructionCache(GetCurrentProcess(), (void*)num, 32u);
|
FlushInstructionCache(GetCurrentProcess(), (void*)num, 32u);
|
||||||
return num;
|
return num;
|
||||||
}
|
}
|
||||||
@@ -2710,10 +2523,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
public void Pump(CpuContext callerContext, string reason)
|
public void Pump(CpuContext callerContext, string reason)
|
||||||
{
|
{
|
||||||
_ = callerContext;
|
_ = callerContext;
|
||||||
if (_guestTeardownRequested)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
var runSynchronously = string.Equals(reason, "entry_return", StringComparison.Ordinal);
|
var runSynchronously = string.Equals(reason, "entry_return", StringComparison.Ordinal);
|
||||||
WakeExpiredBlockedGuestThreads();
|
WakeExpiredBlockedGuestThreads();
|
||||||
if (Volatile.Read(ref _readyGuestThreadCount) == 0)
|
if (Volatile.Read(ref _readyGuestThreadCount) == 0)
|
||||||
@@ -3238,45 +3047,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
continuationThread.Join();
|
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()
|
private void ClearGuestThreads()
|
||||||
{
|
{
|
||||||
GuestContinuationRunner[] runners;
|
GuestContinuationRunner[] runners;
|
||||||
@@ -3556,13 +3326,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
ApplyGuestThreadAffinity(thread.AffinityMask);
|
ApplyGuestThreadAffinity(thread.AffinityMask);
|
||||||
Volatile.Write(ref thread.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
Volatile.Write(ref thread.HostThreadId, unchecked((int)GetCurrentThreadId()));
|
||||||
_activeGuestThreadState = thread;
|
_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
|
try
|
||||||
{
|
{
|
||||||
LastError = null;
|
LastError = null;
|
||||||
@@ -3637,13 +3400,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
}
|
}
|
||||||
finally
|
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;
|
_activeGuestThreadState = previousGuestThreadState;
|
||||||
Volatile.Write(ref thread.HostThreadId, 0);
|
Volatile.Write(ref thread.HostThreadId, 0);
|
||||||
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
|
GuestThreadExecution.RestoreGuestThread(previousGuestThreadHandle);
|
||||||
@@ -3718,175 +3474,169 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
var previousYieldRequested = _activeGuestThreadYieldRequested;
|
var previousYieldRequested = _activeGuestThreadYieldRequested;
|
||||||
var previousYieldReason = _activeGuestThreadYieldReason;
|
var previousYieldReason = _activeGuestThreadYieldReason;
|
||||||
nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex);
|
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
|
try
|
||||||
{
|
{
|
||||||
_activeExecutionBackend = this;
|
_activeExecutionBackend = this;
|
||||||
_activeCpuContext = context;
|
_activeCpuContext = context;
|
||||||
_activeEntryReturnSentinelRip = 0;
|
_activeEntryReturnSentinelRip = 0;
|
||||||
_activeGuestReturnSlotAddress = 0;
|
_activeGuestReturnSlotAddress = 0;
|
||||||
_activeForcedGuestExit = false;
|
_activeForcedGuestExit = false;
|
||||||
_activeGuestThreadYieldRequested = false;
|
_activeGuestThreadYieldRequested = false;
|
||||||
_activeGuestThreadYieldReason = null;
|
_activeGuestThreadYieldReason = null;
|
||||||
BindTlsBase(context);
|
BindTlsBase(context);
|
||||||
byte* ptr2 = (byte*)ptr;
|
byte* ptr2 = (byte*)ptr;
|
||||||
ulong hostRspSlot = (ulong)ptr + stubSize - 16uL;
|
ulong hostRspSlot = (ulong)ptr + stubSize - 16uL;
|
||||||
int offset = 0;
|
int offset = 0;
|
||||||
ptr2[offset++] = 83;
|
ptr2[offset++] = 83;
|
||||||
ptr2[offset++] = 85;
|
ptr2[offset++] = 85;
|
||||||
ptr2[offset++] = 87;
|
ptr2[offset++] = 87;
|
||||||
ptr2[offset++] = 86;
|
ptr2[offset++] = 86;
|
||||||
ptr2[offset++] = 65;
|
ptr2[offset++] = 65;
|
||||||
ptr2[offset++] = 84;
|
ptr2[offset++] = 84;
|
||||||
ptr2[offset++] = 65;
|
ptr2[offset++] = 65;
|
||||||
ptr2[offset++] = 85;
|
ptr2[offset++] = 85;
|
||||||
ptr2[offset++] = 65;
|
ptr2[offset++] = 65;
|
||||||
ptr2[offset++] = 86;
|
ptr2[offset++] = 86;
|
||||||
ptr2[offset++] = 65;
|
ptr2[offset++] = 65;
|
||||||
ptr2[offset++] = 87;
|
ptr2[offset++] = 87;
|
||||||
EmitHostNonvolatileXmmSave(ptr2, ref offset);
|
EmitHostNonvolatileXmmSave(ptr2, ref offset);
|
||||||
ptr2[offset++] = 73;
|
ptr2[offset++] = 73;
|
||||||
ptr2[offset++] = 186;
|
ptr2[offset++] = 186;
|
||||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||||
offset += 8;
|
offset += 8;
|
||||||
ptr2[offset++] = 73;
|
ptr2[offset++] = 73;
|
||||||
ptr2[offset++] = 137;
|
ptr2[offset++] = 137;
|
||||||
ptr2[offset++] = 34;
|
ptr2[offset++] = 34;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 184;
|
ptr2[offset++] = 184;
|
||||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp];
|
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsp];
|
||||||
offset += 8;
|
offset += 8;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 137;
|
ptr2[offset++] = 137;
|
||||||
ptr2[offset++] = 196;
|
ptr2[offset++] = 196;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 131;
|
ptr2[offset++] = 131;
|
||||||
ptr2[offset++] = 236;
|
ptr2[offset++] = 236;
|
||||||
ptr2[offset++] = 8;
|
ptr2[offset++] = 8;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 189;
|
ptr2[offset++] = 189;
|
||||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp];
|
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rbp];
|
||||||
offset += 8;
|
offset += 8;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 184;
|
ptr2[offset++] = 184;
|
||||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi];
|
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdi];
|
||||||
offset += 8;
|
offset += 8;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 137;
|
ptr2[offset++] = 137;
|
||||||
ptr2[offset++] = 199;
|
ptr2[offset++] = 199;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 184;
|
ptr2[offset++] = 184;
|
||||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi];
|
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rsi];
|
||||||
offset += 8;
|
offset += 8;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 137;
|
ptr2[offset++] = 137;
|
||||||
ptr2[offset++] = 198;
|
ptr2[offset++] = 198;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 184;
|
ptr2[offset++] = 184;
|
||||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx];
|
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rdx];
|
||||||
offset += 8;
|
offset += 8;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 137;
|
ptr2[offset++] = 137;
|
||||||
ptr2[offset++] = 194;
|
ptr2[offset++] = 194;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 184;
|
ptr2[offset++] = 184;
|
||||||
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx];
|
*(ulong*)(ptr2 + offset) = context[CpuRegister.Rcx];
|
||||||
offset += 8;
|
offset += 8;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 137;
|
ptr2[offset++] = 137;
|
||||||
ptr2[offset++] = 193;
|
ptr2[offset++] = 193;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 184;
|
ptr2[offset++] = 184;
|
||||||
*(ulong*)(ptr2 + offset) = entryPoint;
|
*(ulong*)(ptr2 + offset) = entryPoint;
|
||||||
offset += 8;
|
offset += 8;
|
||||||
ptr2[offset++] = byte.MaxValue;
|
ptr2[offset++] = byte.MaxValue;
|
||||||
ptr2[offset++] = 208;
|
ptr2[offset++] = 208;
|
||||||
int sentinelOffset = offset + 4;
|
int sentinelOffset = offset + 4;
|
||||||
ptr2[offset++] = 72;
|
ptr2[offset++] = 72;
|
||||||
ptr2[offset++] = 131;
|
ptr2[offset++] = 131;
|
||||||
ptr2[offset++] = 196;
|
ptr2[offset++] = 196;
|
||||||
ptr2[offset++] = 8;
|
ptr2[offset++] = 8;
|
||||||
ptr2[offset++] = 73;
|
ptr2[offset++] = 73;
|
||||||
ptr2[offset++] = 186;
|
ptr2[offset++] = 186;
|
||||||
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
*(ulong*)(ptr2 + offset) = hostRspSlot;
|
||||||
offset += 8;
|
offset += 8;
|
||||||
ptr2[offset++] = 73;
|
ptr2[offset++] = 73;
|
||||||
ptr2[offset++] = 139;
|
ptr2[offset++] = 139;
|
||||||
ptr2[offset++] = 34;
|
ptr2[offset++] = 34;
|
||||||
EmitHostNonvolatileXmmRestore(ptr2, ref offset);
|
EmitHostNonvolatileXmmRestore(ptr2, ref offset);
|
||||||
ptr2[offset++] = 65;
|
ptr2[offset++] = 65;
|
||||||
ptr2[offset++] = 95;
|
ptr2[offset++] = 95;
|
||||||
ptr2[offset++] = 65;
|
ptr2[offset++] = 65;
|
||||||
ptr2[offset++] = 94;
|
ptr2[offset++] = 94;
|
||||||
ptr2[offset++] = 65;
|
ptr2[offset++] = 65;
|
||||||
ptr2[offset++] = 93;
|
ptr2[offset++] = 93;
|
||||||
ptr2[offset++] = 65;
|
ptr2[offset++] = 65;
|
||||||
ptr2[offset++] = 92;
|
ptr2[offset++] = 92;
|
||||||
ptr2[offset++] = 94;
|
ptr2[offset++] = 94;
|
||||||
ptr2[offset++] = 95;
|
ptr2[offset++] = 95;
|
||||||
ptr2[offset++] = 93;
|
ptr2[offset++] = 93;
|
||||||
ptr2[offset++] = 91;
|
ptr2[offset++] = 91;
|
||||||
ptr2[offset++] = 195;
|
ptr2[offset++] = 195;
|
||||||
ulong sentinel = (ulong)ptr + (ulong)sentinelOffset;
|
ulong sentinel = (ulong)ptr + (ulong)sentinelOffset;
|
||||||
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
|
ActiveEntryReturnSentinelRip = (ulong)_guestReturnStub;
|
||||||
_activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL;
|
_activeGuestReturnSlotAddress = context[CpuRegister.Rsp] - 16uL;
|
||||||
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel))
|
if (!context.TryWriteUInt64(context[CpuRegister.Rsp], sentinel))
|
||||||
{
|
{
|
||||||
reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}";
|
reason = $"failed to patch guest thread return sentinel at 0x{context[CpuRegister.Rsp]:X16}";
|
||||||
return GuestNativeCallExitReason.Exception;
|
return GuestNativeCallExitReason.Exception;
|
||||||
}
|
}
|
||||||
uint oldProtect = default(uint);
|
uint oldProtect = default(uint);
|
||||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
|
||||||
{
|
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||||
reason = $"VirtualProtect failed for guest thread entry stub at 0x{(nint)ptr:X16}";
|
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||||
return GuestNativeCallExitReason.Exception;
|
ActiveGuestThreadYieldRequested = false;
|
||||||
}
|
ActiveGuestThreadYieldReason = null;
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
try
|
||||||
ActiveGuestThreadYieldRequested = false;
|
{
|
||||||
ActiveGuestThreadYieldReason = null;
|
var nativeReturn = CallNativeEntry(ptr);
|
||||||
var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot);
|
if (ActiveGuestThreadYieldRequested)
|
||||||
if (ActiveGuestThreadYieldRequested)
|
{
|
||||||
{
|
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
return GuestNativeCallExitReason.Blocked;
|
||||||
return GuestNativeCallExitReason.Blocked;
|
}
|
||||||
}
|
if (ActiveForcedGuestExit)
|
||||||
if (ActiveForcedGuestExit)
|
{
|
||||||
{
|
reason = LastError ?? "guest thread forced exit";
|
||||||
reason = LastError ?? "guest thread forced exit";
|
return GuestNativeCallExitReason.ForcedExit;
|
||||||
return GuestNativeCallExitReason.ForcedExit;
|
}
|
||||||
}
|
reason = $"returned 0x{nativeReturn:X8}";
|
||||||
reason = $"returned 0x{nativeReturn:X8}";
|
return GuestNativeCallExitReason.Returned;
|
||||||
return GuestNativeCallExitReason.Returned;
|
}
|
||||||
}
|
catch (AccessViolationException ex)
|
||||||
catch (AccessViolationException ex)
|
{
|
||||||
{
|
reason = "access violation: " + ex.Message;
|
||||||
reason = "access violation: " + ex.Message;
|
return GuestNativeCallExitReason.Exception;
|
||||||
return GuestNativeCallExitReason.Exception;
|
}
|
||||||
}
|
catch (Exception ex)
|
||||||
catch (Exception ex)
|
{
|
||||||
{
|
reason = ex.GetType().Name + ": " + ex.Message;
|
||||||
reason = ex.GetType().Name + ": " + ex.Message;
|
return GuestNativeCallExitReason.Exception;
|
||||||
return GuestNativeCallExitReason.Exception;
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue);
|
TlsSetValue(_hostRspSlotTlsIndex, previousHostRspSlotValue);
|
||||||
RestoreActiveExecutionThread(
|
RestoreActiveExecutionThread(
|
||||||
previousActiveBackend,
|
previousActiveBackend,
|
||||||
previousActiveContext,
|
previousActiveContext,
|
||||||
previousSentinel,
|
previousSentinel,
|
||||||
previousReturnSlotAddress,
|
previousReturnSlotAddress,
|
||||||
previousForcedExit,
|
previousForcedExit,
|
||||||
previousYieldRequested,
|
previousYieldRequested,
|
||||||
previousYieldReason);
|
previousYieldReason);
|
||||||
VirtualFree(ptr, 0u, 32768u);
|
VirtualFree(ptr, 0u, 32768u);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe GuestNativeCallExitReason ExecuteGuestContinuationEntry(
|
private unsafe GuestNativeCallExitReason ExecuteGuestContinuationEntry(
|
||||||
CpuContext context,
|
CpuContext context,
|
||||||
@@ -3916,12 +3666,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
var previousYieldRequested = _activeGuestThreadYieldRequested;
|
var previousYieldRequested = _activeGuestThreadYieldRequested;
|
||||||
var previousYieldReason = _activeGuestThreadYieldReason;
|
var previousYieldReason = _activeGuestThreadYieldReason;
|
||||||
nint previousHostRspSlotValue = TlsGetValue(_hostRspSlotTlsIndex);
|
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
|
try
|
||||||
{
|
{
|
||||||
_activeExecutionBackend = this;
|
_activeExecutionBackend = this;
|
||||||
@@ -3984,18 +3728,14 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return GuestNativeCallExitReason.Exception;
|
return GuestNativeCallExitReason.Exception;
|
||||||
}
|
}
|
||||||
uint oldProtect = default(uint);
|
uint oldProtect = default(uint);
|
||||||
if (!VirtualProtect(ptr, stubSize, 64u, &oldProtect))
|
VirtualProtect(ptr, stubSize, 64u, &oldProtect);
|
||||||
{
|
|
||||||
reason = $"VirtualProtect failed for guest continuation stub at 0x{(nint)ptr:X16}";
|
|
||||||
return GuestNativeCallExitReason.Exception;
|
|
||||||
}
|
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||||
|
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
|
||||||
ActiveGuestThreadYieldRequested = false;
|
ActiveGuestThreadYieldRequested = false;
|
||||||
ActiveGuestThreadYieldReason = null;
|
ActiveGuestThreadYieldReason = null;
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var nativeReturn = RunGuestEntryStub(ptr, hostRspSlot);
|
var nativeReturn = CallNativeEntry(ptr);
|
||||||
if (ActiveGuestThreadYieldRequested)
|
if (ActiveGuestThreadYieldRequested)
|
||||||
{
|
{
|
||||||
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
reason = ActiveGuestThreadYieldReason ?? "guest thread blocked";
|
||||||
@@ -4248,12 +3988,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
uint num5 = default(uint);
|
uint num5 = default(uint);
|
||||||
if (!VirtualProtect(ptr, stubSize, 64u, &num5))
|
VirtualProtect(ptr, stubSize, 64u, &num5);
|
||||||
{
|
|
||||||
LastError = $"VirtualProtect failed for guest entry stub at 0x{(nint)ptr:X16}";
|
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
FlushInstructionCache(GetCurrentProcess(), ptr, stubSize);
|
||||||
if (_hostRspSlotStorage != 0)
|
if (_hostRspSlotStorage != 0)
|
||||||
{
|
{
|
||||||
@@ -4271,7 +4006,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
int num6 = -1;
|
int num6 = -1;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
num6 = RunGuestEntryStub(ptr, num2);
|
num6 = CallNativeEntry(ptr);
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Guest returned: {num6}");
|
Console.Error.WriteLine($"[LOADER][INFO] Guest returned: {num6}");
|
||||||
PumpUntilGuestThreadsIdle(context, "entry_return");
|
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.";
|
LastError = "Detected repeating import loop and forced guest unwind to host.";
|
||||||
}
|
}
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
||||||
RequestGuestThreadTeardown(3000);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (num6 == 0)
|
if (num6 == 0)
|
||||||
@@ -4313,7 +4047,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
LastError = $"Guest entry point returned non-zero: {num6}";
|
LastError = $"Guest entry point returned non-zero: {num6}";
|
||||||
}
|
}
|
||||||
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
Console.Error.WriteLine("[LOADER][ERROR] " + LastError);
|
||||||
RequestGuestThreadTeardown(3000);
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
@@ -4638,18 +4371,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
|
|||||||
|
|
||||||
public unsafe void Dispose()
|
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();
|
ClearImportHandlerTrampolines();
|
||||||
_importEntries = Array.Empty<ImportStubEntry>();
|
_importEntries = Array.Empty<ImportStubEntry>();
|
||||||
_runtimeSymbolsByName.Clear();
|
_runtimeSymbolsByName.Clear();
|
||||||
|
|||||||
@@ -10,13 +10,11 @@ using SharpEmu.Core;
|
|||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Loader;
|
namespace SharpEmu.Core.Loader;
|
||||||
|
|
||||||
public sealed class SelfLoader : ISelfLoader
|
public sealed class SelfLoader : ISelfLoader
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
|
||||||
private const uint SelfMagic = 0x4F153D1D;
|
private const uint SelfMagic = 0x4F153D1D;
|
||||||
private const ulong SelfSegmentFlag = 0x800;
|
private const ulong SelfSegmentFlag = 0x800;
|
||||||
private const int PageSize = 0x1000;
|
private const int PageSize = 0x1000;
|
||||||
@@ -157,7 +155,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
|
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 loadContext = ParseLayout(imageData);
|
||||||
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
||||||
@@ -257,7 +255,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
_nextTlsModuleId++;
|
_nextTlsModuleId++;
|
||||||
}
|
}
|
||||||
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||||
|
|
||||||
return new SelfImage(
|
return new SelfImage(
|
||||||
loadContext.IsSelf,
|
loadContext.IsSelf,
|
||||||
@@ -405,15 +403,15 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
var virtualAddress = header.VirtualAddress + imageBase;
|
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)
|
if (header.Alignment > 1)
|
||||||
{
|
{
|
||||||
var vaddrMod = virtualAddress % header.Alignment;
|
var vaddrMod = virtualAddress % header.Alignment;
|
||||||
var offsetMod = header.Offset % header.Alignment;
|
var offsetMod = header.Offset % header.Alignment;
|
||||||
if (vaddrMod != offsetMod)
|
if (vaddrMod != offsetMod)
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Segment {index} ELF alignment mismatch! " +
|
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
|
||||||
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
||||||
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod: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)
|
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
Console.Error.WriteLine($"[LOADER] !!! 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] 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 &&
|
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
||||||
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
$"[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))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSupportedRelocationType(relocation.Type))
|
if (!IsSupportedRelocationType(relocation.Type))
|
||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
|
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocation.Type}");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -802,7 +800,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Log.Debug("[FOCUS][SKIP] target address not mapped");
|
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -842,7 +840,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -854,9 +852,10 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||||
if (symbolAddress == 0)
|
if (symbolAddress == 0)
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(new RelocationDescriptor(
|
||||||
@@ -874,9 +873,10 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||||
if (symbolAddress == 0)
|
if (symbolAddress == 0)
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||||
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(new RelocationDescriptor(
|
||||||
@@ -893,7 +893,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||||
{
|
{
|
||||||
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
|
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -902,7 +902,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -950,8 +950,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
||||||
{
|
{
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
$"[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)
|
if (preInitializers.Count != 0 || initializers.Count != 0)
|
||||||
{
|
{
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
$"[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 requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
|
||||||
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||||
if (mapSize > int.MaxValue)
|
if (mapSize > int.MaxValue)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
|
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);
|
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
|
||||||
|
|
||||||
var guestAddr = location + imageBase;
|
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))
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (virtualMemory.TryRead(location, tableBytes))
|
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;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1859,11 +1859,11 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
var slice = elfData.Slice((int)location, (int)size);
|
var slice = elfData.Slice((int)location, (int)size);
|
||||||
tableBytes = slice.ToArray();
|
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;
|
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>();
|
tableBytes = Array.Empty<byte>();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1920,17 +1920,17 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
|
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)
|
if (address == 0)
|
||||||
{
|
{
|
||||||
Log.Debug("-> return 0 (null)");
|
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
|
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;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1939,18 +1939,18 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
var rebased = address + imageBase;
|
var rebased = address + imageBase;
|
||||||
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
|
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;
|
return resolvedRebased;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (address < 0x10000)
|
if (address < 0x10000)
|
||||||
{
|
{
|
||||||
Log.Debug($"-> reject small 0x{address:X}");
|
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug($"-> fallback raw 0x{address:X}");
|
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,19 +4,14 @@
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using SharpEmu.Core.Loader;
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Memory;
|
namespace SharpEmu.Core.Memory;
|
||||||
|
|
||||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
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 ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||||
private readonly object _guestAllocationGate = new();
|
private readonly object _guestAllocationGate = new();
|
||||||
private readonly object _allocationSearchHintGate = new();
|
|
||||||
private readonly List<MemoryRegion> _regions = new();
|
private readonly List<MemoryRegion> _regions = new();
|
||||||
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
|
||||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||||
private bool _disposed;
|
private bool _disposed;
|
||||||
private const ulong PageSize = 0x1000;
|
private const ulong PageSize = 0x1000;
|
||||||
@@ -24,8 +19,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
||||||
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
||||||
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
||||||
private const ulong FullCommitRegionLimit = 4UL << 30;
|
private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
|
||||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
|
||||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||||
|
|
||||||
private const uint MEM_COMMIT = 0x1000;
|
private const uint MEM_COMMIT = 0x1000;
|
||||||
@@ -41,7 +35,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private ulong _guestAllocationArenaBase;
|
private ulong _guestAllocationArenaBase;
|
||||||
private ulong _guestAllocationOffset;
|
private ulong _guestAllocationOffset;
|
||||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||||
@@ -58,11 +51,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
||||||
|
|
||||||
[DllImport("kernel32.dll")]
|
[DllImport("kernel32.dll")]
|
||||||
private static extern void* GetCurrentProcess();
|
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
|
||||||
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
|
||||||
|
|
||||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||||
{
|
{
|
||||||
@@ -92,7 +81,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.EnterWriteLock();
|
_gate.EnterWriteLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
InsertRegionSorted(new MemoryRegion
|
_regions.Add(new MemoryRegion
|
||||||
{
|
{
|
||||||
VirtualAddress = actualAddress,
|
VirtualAddress = actualAddress,
|
||||||
Size = alignedSize,
|
Size = alignedSize,
|
||||||
@@ -107,7 +96,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allocationKind = executable ? "executable memory" : "data memory";
|
var allocationKind = executable ? "executable memory" : "data memory";
|
||||||
TraceVmem($"Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -121,9 +110,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
||||||
var reservedOnly = false;
|
var reservedOnly = false;
|
||||||
var preferReserveOnly = !executable &&
|
var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold;
|
||||||
alignedSize >= LargeDataReserveThreshold &&
|
|
||||||
alignedSize > FullCommitRegionLimit;
|
|
||||||
|
|
||||||
void* result = null;
|
void* result = null;
|
||||||
if (preferReserveOnly)
|
if (preferReserveOnly)
|
||||||
@@ -152,7 +139,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
throw new InvalidOperationException($"Failed to allocate exact mapping at 0x{desiredAddress:X16} ({alignedSize} bytes)");
|
throw new InvalidOperationException($"Failed to allocate exact mapping at 0x{desiredAddress:X16} ({alignedSize} bytes)");
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
Console.Error.WriteLine($"[VMEM] Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
||||||
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
|
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
|
||||||
|
|
||||||
if (result == null)
|
if (result == null)
|
||||||
@@ -206,12 +193,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
lazyPrimeState = committedBytes == primeBytes
|
lazyPrimeState = committedBytes == primeBytes
|
||||||
? $"ok:{committedBytes:X}"
|
? $"ok:{committedBytes:X}"
|
||||||
: $"partial:{committedBytes:X}/{primeBytes:X}";
|
: $"partial:{committedBytes:X}/{primeBytes:X}";
|
||||||
TraceVmem($"Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
Console.Error.WriteLine($"[VMEM] Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
lazyPrimeState = $"fail:{primeBytes:X}";
|
lazyPrimeState = $"fail:{primeBytes:X}";
|
||||||
TraceVmem($"Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
Console.Error.WriteLine($"[VMEM] Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -223,7 +210,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.EnterWriteLock();
|
_gate.EnterWriteLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
InsertRegionSorted(new MemoryRegion
|
_regions.Add(new MemoryRegion
|
||||||
{
|
{
|
||||||
VirtualAddress = actualAddress,
|
VirtualAddress = actualAddress,
|
||||||
Size = alignedSize,
|
Size = alignedSize,
|
||||||
@@ -240,7 +227,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var allocationKind = reservedOnly
|
var allocationKind = reservedOnly
|
||||||
? "reserved data memory (lazy commit)"
|
? "reserved data memory (lazy commit)"
|
||||||
: (executable ? "executable memory" : "data memory");
|
: (executable ? "executable memory" : "data memory");
|
||||||
TraceVmem($"Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
|
Console.Error.WriteLine($"[VMEM] Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
|
||||||
|
|
||||||
return actualAddress;
|
return actualAddress;
|
||||||
}
|
}
|
||||||
@@ -260,8 +247,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
var alignedSize = AlignUp(size, PageSize);
|
var alignedSize = AlignUp(size, PageSize);
|
||||||
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
|
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
|
||||||
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
var cursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
|
||||||
|
|
||||||
for (var attempt = 0; attempt < 0x10000; attempt++)
|
for (var attempt = 0; attempt < 0x10000; attempt++)
|
||||||
{
|
{
|
||||||
@@ -281,7 +267,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
||||||
if (actualAddress == cursor)
|
if (actualAddress == cursor)
|
||||||
{
|
{
|
||||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -349,10 +334,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
_regions.Clear();
|
_regions.Clear();
|
||||||
_pageProtections.Clear();
|
_pageProtections.Clear();
|
||||||
lock (_allocationSearchHintGate)
|
|
||||||
{
|
|
||||||
_allocationSearchHints.Clear();
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -409,7 +390,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
ApplySegmentProtection(mapStart, mapEnd, protection);
|
ApplySegmentProtection(mapStart, mapEnd, protection);
|
||||||
|
|
||||||
TraceVmem($"Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -458,7 +439,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||||
{
|
{
|
||||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
FlushInstructionCache(null, (void*)address, (nuint)size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -492,34 +473,30 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.EnterReadLock();
|
_gate.EnterReadLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
foreach (var region in _regions)
|
||||||
if (region is not null &&
|
|
||||||
TryResolveRegionOffset(
|
|
||||||
virtualAddress,
|
|
||||||
(ulong)destination.Length,
|
|
||||||
region,
|
|
||||||
out var offset))
|
|
||||||
{
|
{
|
||||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
if (TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
||||||
if (destination.IsEmpty)
|
|
||||||
{
|
{
|
||||||
return true;
|
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||||
}
|
if (destination.IsEmpty)
|
||||||
|
|
||||||
if (region.IsReservedOnly)
|
|
||||||
{
|
|
||||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
|
||||||
{
|
{
|
||||||
return false;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (region.IsReservedOnly)
|
||||||
|
{
|
||||||
|
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
|
{
|
||||||
|
requiresExclusiveAccess = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
|
||||||
{
|
|
||||||
requiresExclusiveAccess = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fixed (byte* destPtr = destination)
|
fixed (byte* destPtr = destination)
|
||||||
{
|
{
|
||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||||
@@ -556,34 +533,30 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.EnterReadLock();
|
_gate.EnterReadLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
var region = FindRegion(virtualAddress, (ulong)source.Length);
|
foreach (var region in _regions)
|
||||||
if (region is not null &&
|
|
||||||
TryResolveRegionOffset(
|
|
||||||
virtualAddress,
|
|
||||||
(ulong)source.Length,
|
|
||||||
region,
|
|
||||||
out var offset))
|
|
||||||
{
|
{
|
||||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
if (TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
||||||
if (source.IsEmpty)
|
|
||||||
{
|
{
|
||||||
return true;
|
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||||
}
|
if (source.IsEmpty)
|
||||||
|
|
||||||
if (region.IsReservedOnly)
|
|
||||||
{
|
|
||||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
|
||||||
{
|
{
|
||||||
return false;
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (region.IsReservedOnly)
|
||||||
|
{
|
||||||
|
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||||
|
{
|
||||||
|
requiresExclusiveAccess = true;
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
|
||||||
{
|
|
||||||
requiresExclusiveAccess = true;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
fixed (byte* srcPtr = source)
|
fixed (byte* srcPtr = source)
|
||||||
{
|
{
|
||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
@@ -616,14 +589,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||||
{
|
{
|
||||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
foreach (var region in _regions)
|
||||||
if (region is not null &&
|
|
||||||
TryResolveRegionOffset(
|
|
||||||
virtualAddress,
|
|
||||||
(ulong)destination.Length,
|
|
||||||
region,
|
|
||||||
out var offset))
|
|
||||||
{
|
{
|
||||||
|
if (!TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
{
|
{
|
||||||
@@ -665,14 +637,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
|
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
var region = FindRegion(virtualAddress, (ulong)source.Length);
|
foreach (var region in _regions)
|
||||||
if (region is not null &&
|
|
||||||
TryResolveRegionOffset(
|
|
||||||
virtualAddress,
|
|
||||||
(ulong)source.Length,
|
|
||||||
region,
|
|
||||||
out var offset))
|
|
||||||
{
|
{
|
||||||
|
if (!TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||||
{
|
{
|
||||||
@@ -706,7 +677,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||||
if (IsExecutableProtection(oldProtect))
|
if (IsExecutableProtection(oldProtect))
|
||||||
{
|
{
|
||||||
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -728,9 +699,15 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.EnterReadLock();
|
_gate.EnterReadLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return FindRegion(virtualAddress, 1) is not null
|
foreach (var region in _regions)
|
||||||
? (void*)virtualAddress
|
{
|
||||||
: null;
|
if (virtualAddress >= region.VirtualAddress &&
|
||||||
|
virtualAddress < region.VirtualAddress + region.Size)
|
||||||
|
{
|
||||||
|
return (void*)virtualAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -743,7 +720,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
_gate.EnterReadLock();
|
_gate.EnterReadLock();
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
return FindRegion(virtualAddress, size) is not null;
|
foreach (var region in _regions)
|
||||||
|
{
|
||||||
|
if (TryResolveRegionOffset(virtualAddress, size, region, out _))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -753,48 +737,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
private MemoryRegion? FindRegion(ulong address, ulong size)
|
private MemoryRegion? FindRegion(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
var low = 0;
|
foreach (var region in _regions)
|
||||||
var high = _regions.Count - 1;
|
|
||||||
MemoryRegion? candidate = null;
|
|
||||||
while (low <= high)
|
|
||||||
{
|
{
|
||||||
var middle = low + ((high - low) >> 1);
|
if (TryResolveRegionOffset(address, size, region, out _))
|
||||||
var region = _regions[middle];
|
|
||||||
if (region.VirtualAddress <= address)
|
|
||||||
{
|
{
|
||||||
candidate = region;
|
return region;
|
||||||
low = middle + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
high = middle - 1;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return null;
|
||||||
return candidate is not null &&
|
|
||||||
TryResolveRegionOffset(address, size, candidate, out _)
|
|
||||||
? candidate
|
|
||||||
: null;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void InsertRegionSorted(MemoryRegion region)
|
|
||||||
{
|
|
||||||
var low = 0;
|
|
||||||
var high = _regions.Count;
|
|
||||||
while (low < high)
|
|
||||||
{
|
|
||||||
var middle = low + ((high - low) >> 1);
|
|
||||||
if (_regions[middle].VirtualAddress < region.VirtualAddress)
|
|
||||||
{
|
|
||||||
low = middle + 1;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
high = middle;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
_regions.Insert(low, region);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private bool TryGetOverlappingRegionEnd(ulong address, ulong size, out ulong overlapEnd)
|
private bool TryGetOverlappingRegionEnd(ulong address, ulong size, out ulong overlapEnd)
|
||||||
@@ -812,16 +762,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
var regionEnd = region.VirtualAddress + region.Size;
|
var regionEnd = region.VirtualAddress + region.Size;
|
||||||
if (region.VirtualAddress >= end)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (regionEnd <= address)
|
|
||||||
{
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (address < regionEnd && region.VirtualAddress < end)
|
if (address < regionEnd && region.VirtualAddress < end)
|
||||||
{
|
{
|
||||||
overlapEnd = Math.Max(overlapEnd, regionEnd);
|
overlapEnd = Math.Max(overlapEnd, regionEnd);
|
||||||
@@ -836,37 +776,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return overlapEnd != 0;
|
return overlapEnd != 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private ulong GetAllocationSearchCursor(
|
|
||||||
ulong desiredAddress,
|
|
||||||
ulong requestedCursor,
|
|
||||||
ulong alignment,
|
|
||||||
bool executable)
|
|
||||||
{
|
|
||||||
lock (_allocationSearchHintGate)
|
|
||||||
{
|
|
||||||
var key = (desiredAddress, alignment, executable);
|
|
||||||
if (_allocationSearchHints.TryGetValue(key, out var hintedCursor) &&
|
|
||||||
hintedCursor > requestedCursor)
|
|
||||||
{
|
|
||||||
return AlignUp(hintedCursor, alignment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return requestedCursor;
|
|
||||||
}
|
|
||||||
|
|
||||||
private void UpdateAllocationSearchCursor(
|
|
||||||
ulong desiredAddress,
|
|
||||||
ulong alignment,
|
|
||||||
bool executable,
|
|
||||||
ulong nextCursor)
|
|
||||||
{
|
|
||||||
lock (_allocationSearchHintGate)
|
|
||||||
{
|
|
||||||
_allocationSearchHints[(desiredAddress, alignment, executable)] = AlignUp(nextCursor, alignment);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
|
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
|
||||||
{
|
{
|
||||||
offset = 0;
|
offset = 0;
|
||||||
@@ -1035,29 +944,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return checked((value + mask) & ~mask);
|
return checked((value + mask) & ~mask);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong ResolveLazyReservePrimeBytes()
|
|
||||||
{
|
|
||||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_LAZY_RESERVE_PRIME_MB");
|
|
||||||
if (ulong.TryParse(configured, out var megabytes))
|
|
||||||
{
|
|
||||||
return megabytes == 0
|
|
||||||
? 0
|
|
||||||
: checked(Math.Min(megabytes, 4096UL) * 1024UL * 1024UL);
|
|
||||||
}
|
|
||||||
|
|
||||||
return DefaultLazyReservePrimeBytes;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceVmem(string message)
|
|
||||||
{
|
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
Log.Debug(message);
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
public void Dispose()
|
||||||
{
|
{
|
||||||
if (!_disposed)
|
if (!_disposed)
|
||||||
|
|||||||
@@ -11,19 +11,15 @@ using SharpEmu.Libs.VideoOut;
|
|||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
using SharpEmu.Libs.AppContent;
|
using SharpEmu.Libs.AppContent;
|
||||||
using SharpEmu.Libs.SaveData;
|
using SharpEmu.Libs.SaveData;
|
||||||
using SharpEmu.Logging;
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using System.IO;
|
|
||||||
|
|
||||||
namespace SharpEmu.Core.Runtime;
|
namespace SharpEmu.Core.Runtime;
|
||||||
|
|
||||||
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
|
|
||||||
|
|
||||||
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
||||||
|
|
||||||
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
||||||
@@ -132,7 +128,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||||
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
||||||
Log.Info($"Loading: {ebootPath}");
|
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||||
LastExecutionDiagnostics = null;
|
LastExecutionDiagnostics = null;
|
||||||
LastExecutionTrace = null;
|
LastExecutionTrace = null;
|
||||||
LastSessionSummary = null;
|
LastSessionSummary = null;
|
||||||
@@ -140,12 +136,11 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
LastMilestoneLog = null;
|
LastMilestoneLog = null;
|
||||||
KernelModuleRegistry.Reset();
|
KernelModuleRegistry.Reset();
|
||||||
var image = LoadImage(normalizedEbootPath);
|
var image = LoadImage(normalizedEbootPath);
|
||||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
|
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||||
LogAppBundleInfo(normalizedEbootPath, image);
|
|
||||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
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 generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
||||||
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
||||||
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
||||||
@@ -168,7 +163,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName);
|
processImageName);
|
||||||
if (initializerResult is { } failedInitializerResult)
|
if (initializerResult is { } failedInitializerResult)
|
||||||
{
|
{
|
||||||
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -176,8 +171,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return failedInitializerResult;
|
return failedInitializerResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info($"Dispatching, gen: {generation}");
|
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
||||||
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
@@ -187,8 +182,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName,
|
processImageName,
|
||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
|
|
||||||
Log.Info($"DispatchEntry returned: {result}");
|
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||||
Log.Info($"Dispatch result: {result}");
|
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -359,66 +354,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return result;
|
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)
|
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
||||||
{
|
{
|
||||||
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
||||||
@@ -499,8 +434,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
moduleName = $"module#{i}";
|
moduleName = $"module#{i}";
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||||
initEntryPoint,
|
initEntryPoint,
|
||||||
@@ -511,8 +446,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
Log.Error(
|
Console.Error.WriteLine(
|
||||||
$"Module start failed: {moduleName} -> {result}");
|
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -533,8 +468,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||||
|
|
||||||
var result = RunInitializerList(
|
var result = RunInitializerList(
|
||||||
$"{label}:preinit",
|
$"{label}:preinit",
|
||||||
@@ -573,8 +508,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
initializerAddress,
|
initializerAddress,
|
||||||
@@ -608,7 +543,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
Path.Combine(ebootDirectory, "sce_module"),
|
Path.Combine(ebootDirectory, "sce_module"),
|
||||||
Path.Combine(ebootDirectory, "sce_modules"),
|
Path.Combine(ebootDirectory, "sce_modules"),
|
||||||
Path.Combine(ebootDirectory, "Media", "Modules"),
|
|
||||||
}
|
}
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
.Where(Directory.Exists)
|
.Where(Directory.Exists)
|
||||||
@@ -620,9 +554,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allModulePaths = moduleDirectories
|
var allModulePaths = moduleDirectories
|
||||||
.SelectMany(directory => Directory
|
.SelectMany(Directory.EnumerateFiles)
|
||||||
.EnumerateFiles(directory)
|
|
||||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
|
||||||
.Where(path =>
|
.Where(path =>
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
@@ -630,6 +562,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
||||||
})
|
})
|
||||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||||
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||||
.ToArray();
|
.ToArray();
|
||||||
|
|
||||||
var modulePaths = allModulePaths
|
var modulePaths = allModulePaths
|
||||||
@@ -642,7 +575,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
if (skippedModules.Length > 0)
|
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)
|
if (modulePaths.Length == 0)
|
||||||
@@ -650,8 +583,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||||
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
||||||
var loadedModules = 0;
|
var loadedModules = 0;
|
||||||
var failedModules = 0;
|
var failedModules = 0;
|
||||||
var mergedImportCount = 0;
|
var mergedImportCount = 0;
|
||||||
@@ -686,18 +619,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
||||||
loadedModules++;
|
loadedModules++;
|
||||||
|
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
failedModules++;
|
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(
|
Console.Error.WriteLine(
|
||||||
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -717,8 +650,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (rebound != 0 || unresolved != 0)
|
if (rebound != 0 || unresolved != 0)
|
||||||
{
|
{
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -750,8 +683,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -763,8 +696,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Error(
|
Console.Error.WriteLine(
|
||||||
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -773,8 +706,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Log.Debug(
|
Console.Error.WriteLine(
|
||||||
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
rebound++;
|
rebound++;
|
||||||
@@ -810,8 +743,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
Log.Warning(
|
Console.Error.WriteLine(
|
||||||
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -920,8 +853,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
isMain,
|
isMain,
|
||||||
isSystemModule);
|
isSystemModule);
|
||||||
Log.Info(
|
Console.Error.WriteLine(
|
||||||
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
$"[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)
|
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
<!--
|
<!--
|
||||||
Copyright (C) 2026 SharpEmu Emulator Project
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
SPDX-License-Identifier: GPL-2.0-or-later
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
-->
|
-->
|
||||||
@@ -14,10 +14,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<PackageReference Include="Iced" />
|
<PackageReference Include="Iced" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -65,17 +65,13 @@
|
|||||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
},
|
},
|
||||||
"sharpemu.hle": {
|
"sharpemu.hle": {
|
||||||
"type": "Project",
|
"type": "Project"
|
||||||
"dependencies": {
|
|
||||||
"SharpEmu.Logging": "[1.0.0, )"
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
"sharpemu.libs": {
|
"sharpemu.libs": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"SharpEmu.HLE": "[1.0.0, )",
|
"SharpEmu.HLE": "[1.0.0, )",
|
||||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||||
"Silk.NET.Windowing": "[2.23.0, )"
|
"Silk.NET.Windowing": "[2.23.0, )"
|
||||||
}
|
}
|
||||||
@@ -92,16 +88,6 @@
|
|||||||
"Silk.NET.Core": "2.23.0"
|
"Silk.NET.Core": "2.23.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
|
||||||
"type": "CentralTransitive",
|
|
||||||
"requested": "[2.23.0, )",
|
|
||||||
"resolved": "2.23.0",
|
|
||||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
|
||||||
"dependencies": {
|
|
||||||
"Silk.NET.Core": "2.23.0",
|
|
||||||
"Silk.NET.Vulkan": "2.23.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||||
"type": "CentralTransitive",
|
"type": "CentralTransitive",
|
||||||
"requested": "[2.23.0, )",
|
"requested": "[2.23.0, )",
|
||||||
|
|||||||
@@ -1,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,150 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using Avalonia;
|
|
||||||
using Avalonia.Collections;
|
|
||||||
using Avalonia.Controls;
|
|
||||||
using Avalonia.Controls.Templates;
|
|
||||||
using Avalonia.Data;
|
|
||||||
using Avalonia.Layout;
|
|
||||||
using Avalonia.Media;
|
|
||||||
using Avalonia.Platform;
|
|
||||||
using Avalonia.Threading;
|
|
||||||
using System.Collections.Specialized;
|
|
||||||
|
|
||||||
namespace SharpEmu.GUI;
|
|
||||||
|
|
||||||
public sealed class ConsoleWindow : Window
|
|
||||||
{
|
|
||||||
private readonly AvaloniaList<LogLine> _sourceLines;
|
|
||||||
private readonly AvaloniaList<LogLine> _visibleLines = new();
|
|
||||||
private readonly ListBox _list;
|
|
||||||
private readonly TextBox _searchBox;
|
|
||||||
private readonly CheckBox _autoScrollCheck;
|
|
||||||
|
|
||||||
public ConsoleWindow(
|
|
||||||
AvaloniaList<LogLine> lines,
|
|
||||||
Action clear,
|
|
||||||
bool autoScroll)
|
|
||||||
{
|
|
||||||
_sourceLines = lines;
|
|
||||||
Title = "SharpEmu Console";
|
|
||||||
Width = 980;
|
|
||||||
Height = 620;
|
|
||||||
MinWidth = 520;
|
|
||||||
MinHeight = 320;
|
|
||||||
Background = new SolidColorBrush(Color.Parse("#0D1017"));
|
|
||||||
Icon = new WindowIcon(AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/SharpEmu.ico")));
|
|
||||||
|
|
||||||
_searchBox = new TextBox { Watermark = "Search...", Width = 320, Margin = new Thickness(0, 0, 12, 0) };
|
|
||||||
_autoScrollCheck = new CheckBox
|
|
||||||
{
|
|
||||||
Content = "Auto-scroll",
|
|
||||||
IsChecked = autoScroll,
|
|
||||||
FontSize = 12,
|
|
||||||
Margin = new Thickness(0, 0, 12, 0),
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
};
|
|
||||||
var copyButton = new Button
|
|
||||||
{
|
|
||||||
Classes = { "ghost" },
|
|
||||||
Content = "Copy",
|
|
||||||
Padding = new Thickness(10, 4),
|
|
||||||
Margin = new Thickness(0, 0, 8, 0),
|
|
||||||
};
|
|
||||||
var clearButton = new Button { Classes = { "ghost" }, Content = "Clear", Padding = new Thickness(10, 4) };
|
|
||||||
copyButton.Click += async (_, _) => await CopyAsync();
|
|
||||||
clearButton.Click += (_, _) => clear();
|
|
||||||
_searchBox.TextChanged += (_, _) => RefreshVisibleLines();
|
|
||||||
|
|
||||||
_list = new ListBox
|
|
||||||
{
|
|
||||||
Classes = { "console" },
|
|
||||||
ItemsSource = _visibleLines,
|
|
||||||
BorderThickness = new Thickness(1),
|
|
||||||
BorderBrush = new SolidColorBrush(Color.Parse("#232B3A")),
|
|
||||||
ItemTemplate = new FuncDataTemplate<LogLine>((_, _) =>
|
|
||||||
{
|
|
||||||
var text = new TextBlock { TextWrapping = TextWrapping.NoWrap };
|
|
||||||
text.Bind(TextBlock.TextProperty, new Binding(nameof(LogLine.Text)));
|
|
||||||
text.Bind(TextBlock.ForegroundProperty, new Binding(nameof(LogLine.Brush)));
|
|
||||||
return text;
|
|
||||||
}),
|
|
||||||
};
|
|
||||||
|
|
||||||
Content = new Grid
|
|
||||||
{
|
|
||||||
Margin = new Thickness(12),
|
|
||||||
RowDefinitions = new RowDefinitions("Auto,*"),
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new Grid
|
|
||||||
{
|
|
||||||
Margin = new Thickness(0, 0, 0, 8),
|
|
||||||
ColumnDefinitions = new ColumnDefinitions("*,Auto,Auto,Auto,Auto"),
|
|
||||||
Children =
|
|
||||||
{
|
|
||||||
new TextBlock
|
|
||||||
{
|
|
||||||
Classes = { "sectionTitle" },
|
|
||||||
Text = "CONSOLE",
|
|
||||||
VerticalAlignment = VerticalAlignment.Center,
|
|
||||||
},
|
|
||||||
_searchBox.WithGridColumn(1),
|
|
||||||
_autoScrollCheck.WithGridColumn(2),
|
|
||||||
copyButton.WithGridColumn(3),
|
|
||||||
clearButton.WithGridColumn(4),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
_list.WithGridRow(1),
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
lines.CollectionChanged += OnLinesChanged;
|
|
||||||
Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged;
|
|
||||||
RefreshVisibleLines();
|
|
||||||
}
|
|
||||||
|
|
||||||
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
|
||||||
{
|
|
||||||
RefreshVisibleLines();
|
|
||||||
if (_autoScrollCheck.IsChecked == true)
|
|
||||||
{
|
|
||||||
Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void RefreshVisibleLines()
|
|
||||||
{
|
|
||||||
var query = _searchBox.Text ?? string.Empty;
|
|
||||||
_visibleLines.Clear();
|
|
||||||
_visibleLines.AddRange(string.IsNullOrWhiteSpace(query)
|
|
||||||
? _sourceLines
|
|
||||||
: _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase)));
|
|
||||||
}
|
|
||||||
|
|
||||||
private async Task CopyAsync()
|
|
||||||
{
|
|
||||||
if (_visibleLines.Count == 0 || Clipboard is null)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text)));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
file static class GridExtensions
|
|
||||||
{
|
|
||||||
public static T WithGridColumn<T>(this T control, int column) where T : Control
|
|
||||||
{
|
|
||||||
Grid.SetColumn(control, column);
|
|
||||||
return control;
|
|
||||||
}
|
|
||||||
|
|
||||||
public static T WithGridRow<T>(this T control, int row) where T : Control
|
|
||||||
{
|
|
||||||
Grid.SetRow(control, row);
|
|
||||||
return control;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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,291 +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,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..." Width="320" />
|
|
||||||
<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="DetachConsoleButton" Classes="ghost" Content="Split" FontSize="12"
|
|
||||||
Padding="10,4" Margin="0,0,8,0" />
|
|
||||||
<Button Grid.Column="4" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
|
||||||
Padding="10,4" Margin="0,0,8,0" />
|
|
||||||
<Button Grid.Column="5" 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"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,13 +3,11 @@
|
|||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
public sealed class Aerolib : ISymbolCatalog
|
public sealed class Aerolib : ISymbolCatalog
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Aerolib");
|
|
||||||
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
|
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
|
||||||
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
|
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
|
||||||
|
|
||||||
@@ -110,14 +108,14 @@ public sealed class Aerolib : ISymbolCatalog
|
|||||||
|
|
||||||
if (resourceName == null)
|
if (resourceName == null)
|
||||||
{
|
{
|
||||||
Log.Error("Embedded resource 'aerolib.bin' not found");
|
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
if (stream == null)
|
if (stream == null)
|
||||||
{
|
{
|
||||||
Log.Error("Failed to open embedded resource stream");
|
Console.Error.WriteLine("[AEROLIB] Failed to open embedded resource stream");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -147,11 +145,11 @@ public sealed class Aerolib : ISymbolCatalog
|
|||||||
_byExportName[name] = symbol;
|
_byExportName[name] = symbol;
|
||||||
}
|
}
|
||||||
|
|
||||||
Log.Info($"Loaded {_byNid.Count} NID entries from binary resource");
|
Console.Error.WriteLine($"[AEROLIB] Loaded {_byNid.Count} NID entries from binary resource");
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Log.Error($"Failed to load embedded aerolib.bin: {ex.Message}", ex);
|
Console.Error.WriteLine($"[AEROLIB] Failed to load embedded aerolib.bin: {ex.Message}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,20 +2,25 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Text;
|
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
public sealed class CpuContext
|
||||||
{
|
{
|
||||||
private readonly ulong[] _registers = new ulong[16];
|
private readonly ulong[] _registers = new ulong[16];
|
||||||
private readonly ulong[] _xmmRegisters = new ulong[32];
|
private readonly ulong[] _xmmRegisters = new ulong[32];
|
||||||
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
|
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
|
||||||
private bool _raxWritten;
|
private bool _raxWritten;
|
||||||
|
|
||||||
public ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory));
|
public CpuContext(ICpuMemory memory, Generation generation)
|
||||||
|
{
|
||||||
|
Memory = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||||
|
TargetGeneration = generation;
|
||||||
|
}
|
||||||
|
|
||||||
public Generation TargetGeneration { get; } = generation;
|
public ICpuMemory Memory { get; }
|
||||||
|
|
||||||
|
public Generation TargetGeneration { get; }
|
||||||
|
|
||||||
public ulong Rip { get; set; }
|
public ulong Rip { get; set; }
|
||||||
|
|
||||||
@@ -125,91 +130,6 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
SetYmmUpper(registerIndex, highLow, highHigh);
|
SetYmmUpper(registerIndex, highLow, highHigh);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryReadByte(ulong address, out byte value)
|
|
||||||
{
|
|
||||||
Span<byte> buffer = stackalloc byte[1];
|
|
||||||
if (!Memory.TryRead(address, buffer))
|
|
||||||
{
|
|
||||||
value = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
value = buffer[0];
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool 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)
|
public bool TryReadUInt64(ulong address, out ulong value)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
@@ -230,33 +150,6 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
return Memory.TryWrite(address, buffer);
|
return Memory.TryWrite(address, buffer);
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryReadNullTerminatedUtf8(ulong address, int capacity, out string value)
|
|
||||||
{
|
|
||||||
value = string.Empty;
|
|
||||||
if (address == 0 || capacity <= 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bytes = new byte[capacity];
|
|
||||||
for (var index = 0; index < bytes.Length; index++)
|
|
||||||
{
|
|
||||||
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (bytes[index] == 0)
|
|
||||||
{
|
|
||||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
value = Encoding.UTF8.GetString(bytes);
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool PushUInt64(ulong value)
|
public bool PushUInt64(ulong value)
|
||||||
{
|
{
|
||||||
var rsp = this[CpuRegister.Rsp];
|
var rsp = this[CpuRegister.Rsp];
|
||||||
@@ -276,23 +169,4 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
|||||||
this[CpuRegister.Rsp] = rsp + sizeof(ulong);
|
this[CpuRegister.Rsp] = rsp + sizeof(ulong);
|
||||||
return true;
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,14 +3,11 @@
|
|||||||
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
using SharpEmu.Logging;
|
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
public sealed class ModuleManager : IModuleManager
|
public sealed class ModuleManager : IModuleManager
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("HLE");
|
|
||||||
|
|
||||||
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||||
@@ -50,7 +47,6 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
var handler = CreateHandler(type, method, instances);
|
var handler = CreateHandler(type, method, instances);
|
||||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||||
{
|
{
|
||||||
Log.Warning($"Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -109,7 +105,6 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
|
|
||||||
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||||
{
|
{
|
||||||
Log.Warning($"NID '{nid}' not found in dispatch table.");
|
|
||||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
return false;
|
return false;
|
||||||
@@ -117,13 +112,11 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
|
|
||||||
if ((export.Target & context.TargetGeneration) == 0)
|
if ((export.Target & context.TargetGeneration) == 0)
|
||||||
{
|
{
|
||||||
Log.Warning($"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_FOUND);
|
||||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
context.ClearRaxWriteFlag();
|
context.ClearRaxWriteFlag();
|
||||||
int ret = ((SysAbiFunction)function).Invoke(context);
|
int ret = ((SysAbiFunction)function).Invoke(context);
|
||||||
|
|
||||||
|
|||||||
@@ -40,13 +40,6 @@ public enum OrbisGen2Result : int
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
ORBIS_GEN2_ERROR_DEADLOCK = unchecked((int)0x8002000B),
|
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>
|
/// <summary>
|
||||||
/// Indicates that the target resource is busy.
|
/// Indicates that the target resource is busy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -57,13 +50,6 @@ public enum OrbisGen2Result : int
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
ORBIS_GEN2_ERROR_TRY_AGAIN = unchecked((int)0x80020023),
|
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>
|
/// <summary>
|
||||||
/// Indicates that behavior is recognized but not implemented yet.
|
/// Indicates that behavior is recognized but not implemented yet.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -8,14 +8,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
|
||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
|
||||||
</PropertyGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
|
||||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
|
||||||
</ItemGroup>
|
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,10 +1,6 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net10.0": {
|
"net10.0": {}
|
||||||
"sharpemu.logging": {
|
|
||||||
"type": "Project"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+514
-883
File diff suppressed because it is too large
Load Diff
@@ -2,6 +2,7 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
@@ -17,9 +18,9 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
out Gen5ShaderMetadata metadata)
|
out Gen5ShaderMetadata metadata)
|
||||||
{
|
{
|
||||||
metadata = default!;
|
metadata = default!;
|
||||||
if (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
if (!TryReadUInt64(ctx, shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
||||||
userDataAddress == 0 ||
|
userDataAddress == 0 ||
|
||||||
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
|
!TryReadUInt64(ctx, userDataAddress, out var directResourceOffsetsAddress))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -27,7 +28,8 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
var resourceOffsets = new ulong[ResourceClassCount];
|
var resourceOffsets = new ulong[ResourceClassCount];
|
||||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt64(
|
if (!TryReadUInt64(
|
||||||
|
ctx,
|
||||||
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
|
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
|
||||||
out resourceOffsets[resourceClass]))
|
out resourceOffsets[resourceClass]))
|
||||||
{
|
{
|
||||||
@@ -35,9 +37,9 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt16(userDataAddress + 0x28, out var extendedUserDataSize) ||
|
if (!TryReadUInt16(ctx, userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||||
!ctx.TryReadUInt16(userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
!TryReadUInt16(ctx, userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||||
!ctx.TryReadUInt16(userDataAddress + 0x2C, out var directResourceCount) ||
|
!TryReadUInt16(ctx, userDataAddress + 0x2C, out var directResourceCount) ||
|
||||||
directResourceCount > MaxMetadataEntries)
|
directResourceCount > MaxMetadataEntries)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -46,7 +48,8 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
var resourceCounts = new ushort[ResourceClassCount];
|
var resourceCounts = new ushort[ResourceClassCount];
|
||||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt16(
|
if (!TryReadUInt16(
|
||||||
|
ctx,
|
||||||
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
|
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
|
||||||
out resourceCounts[resourceClass]) ||
|
out resourceCounts[resourceClass]) ||
|
||||||
resourceCounts[resourceClass] > MaxMetadataEntries)
|
resourceCounts[resourceClass] > MaxMetadataEntries)
|
||||||
@@ -65,7 +68,7 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
|
|
||||||
for (uint type = 0; type < directResourceCount; type++)
|
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;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -93,7 +96,8 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
|
|
||||||
for (uint slot = 0; slot < count; slot++)
|
for (uint slot = 0; slot < count; slot++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt16(
|
if (!TryReadUInt16(
|
||||||
|
ctx,
|
||||||
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
||||||
out var sharp))
|
out var sharp))
|
||||||
{
|
{
|
||||||
@@ -121,4 +125,30 @@ internal static class Gen5ShaderMetadataReader
|
|||||||
resources);
|
resources);
|
||||||
return true;
|
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.HLE;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Numerics;
|
using System.Numerics;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
@@ -1404,7 +1405,8 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(
|
if (!TryReadUInt32(
|
||||||
|
ctx,
|
||||||
address + (ulong)(index * sizeof(uint)),
|
address + (ulong)(index * sizeof(uint)),
|
||||||
out var value) &&
|
out var value) &&
|
||||||
!TryReadUserDataScalarLoad(
|
!TryReadUserDataScalarLoad(
|
||||||
@@ -1725,4 +1727,16 @@ internal static class Gen5ShaderScalarEvaluator
|
|||||||
opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
|
opcode.StartsWith("ImageSample", StringComparison.Ordinal) ||
|
||||||
opcode.StartsWith("ImageGather", StringComparison.Ordinal);
|
opcode.StartsWith("ImageGather", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -4,7 +4,6 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.VideoOut;
|
using SharpEmu.Libs.VideoOut;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Runtime.CompilerServices;
|
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Agc;
|
namespace SharpEmu.Libs.Agc;
|
||||||
@@ -14,14 +13,6 @@ internal static class Gen5ShaderTranslator
|
|||||||
private const int MaxInstructions = 4096;
|
private const int MaxInstructions = 4096;
|
||||||
private const int MinimumUserDataDwords = 16;
|
private const int MinimumUserDataDwords = 16;
|
||||||
private const int MaximumUserDataDwords = 256;
|
private const int MaximumUserDataDwords = 256;
|
||||||
private static readonly ConditionalWeakTable<object, ShaderDecodeCache> _decodeCaches = new();
|
|
||||||
|
|
||||||
private sealed class ShaderDecodeCache
|
|
||||||
{
|
|
||||||
public object Gate { get; } = new();
|
|
||||||
public Dictionary<ulong, Gen5ShaderProgram> Programs { get; } = new();
|
|
||||||
public Dictionary<ulong, Gen5ShaderMetadata?> Metadata { get; } = new();
|
|
||||||
}
|
|
||||||
|
|
||||||
private static readonly uint[] FullscreenBarycentricEs =
|
private static readonly uint[] FullscreenBarycentricEs =
|
||||||
[
|
[
|
||||||
@@ -126,51 +117,16 @@ internal static class Gen5ShaderTranslator
|
|||||||
uint userDataScalarRegisterBase = 0)
|
uint userDataScalarRegisterBase = 0)
|
||||||
{
|
{
|
||||||
state = default!;
|
state = default!;
|
||||||
error = string.Empty;
|
if (!TryDecodeProgram(ctx, shaderAddress, out var program, out error))
|
||||||
var cache = _decodeCaches.GetValue(ctx.Memory, static _ => new ShaderDecodeCache());
|
|
||||||
Gen5ShaderProgram? program;
|
|
||||||
lock (cache.Gate)
|
|
||||||
{
|
{
|
||||||
cache.Programs.TryGetValue(shaderAddress, out program);
|
return false;
|
||||||
}
|
|
||||||
|
|
||||||
if (program is null)
|
|
||||||
{
|
|
||||||
if (!TryDecodeProgram(ctx, shaderAddress, out program, out error))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (cache.Gate)
|
|
||||||
{
|
|
||||||
cache.Programs.TryAdd(shaderAddress, program);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Gen5ShaderMetadata? metadata = null;
|
Gen5ShaderMetadata? metadata = null;
|
||||||
if (shaderHeaderAddress != 0)
|
if (shaderHeaderAddress != 0 &&
|
||||||
|
Gen5ShaderMetadataReader.TryRead(ctx, shaderHeaderAddress, out var decodedMetadata))
|
||||||
{
|
{
|
||||||
var metadataCached = false;
|
metadata = decodedMetadata;
|
||||||
lock (cache.Gate)
|
|
||||||
{
|
|
||||||
metadataCached = cache.Metadata.TryGetValue(shaderHeaderAddress, out metadata);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!metadataCached)
|
|
||||||
{
|
|
||||||
if (Gen5ShaderMetadataReader.TryRead(
|
|
||||||
ctx,
|
|
||||||
shaderHeaderAddress,
|
|
||||||
out var decodedMetadata))
|
|
||||||
{
|
|
||||||
metadata = decodedMetadata;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (cache.Gate)
|
|
||||||
{
|
|
||||||
cache.Metadata.TryAdd(shaderHeaderAddress, metadata);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var userData = new uint[GetUserDataDwordCount(metadata)];
|
var userData = new uint[GetUserDataDwordCount(metadata)];
|
||||||
@@ -294,7 +250,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
var instructionCount = 0;
|
var instructionCount = 0;
|
||||||
for (uint pc = 0; instructionCount < MaxInstructions;)
|
for (uint pc = 0; instructionCount < MaxInstructions;)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(address + pc, out var word))
|
if (!TryReadUInt32(ctx, address + pc, out var word))
|
||||||
{
|
{
|
||||||
error = $"read-failed pc=0x{pc:X}";
|
error = $"read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -317,7 +273,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
words[0] = word;
|
words[0] = word;
|
||||||
for (uint wordIndex = 1; wordIndex < sizeDwords; wordIndex++)
|
for (uint wordIndex = 1; wordIndex < sizeDwords; wordIndex++)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(address + pc + wordIndex * sizeof(uint), out words[wordIndex]))
|
if (!TryReadUInt32(ctx, address + pc + wordIndex * sizeof(uint), out words[wordIndex]))
|
||||||
{
|
{
|
||||||
error = $"read-failed pc=0x{pc + wordIndex * sizeof(uint):X}";
|
error = $"read-failed pc=0x{pc + wordIndex * sizeof(uint):X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -397,7 +353,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
case 0x34:
|
case 0x34:
|
||||||
case 0x35:
|
case 0x35:
|
||||||
encoding = Gen5ShaderEncoding.Vop3;
|
encoding = Gen5ShaderEncoding.Vop3;
|
||||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var vop3Extra))
|
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var vop3Extra))
|
||||||
{
|
{
|
||||||
error = $"vop3-extra-read-failed pc=0x{pc:X}";
|
error = $"vop3-extra-read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -418,7 +374,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
return DecodeFlat(word, out name, out sizeDwords, out error);
|
return DecodeFlat(word, out name, out sizeDwords, out error);
|
||||||
case 0x38:
|
case 0x38:
|
||||||
encoding = Gen5ShaderEncoding.Mubuf;
|
encoding = Gen5ShaderEncoding.Mubuf;
|
||||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var mubufExtra))
|
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mubufExtra))
|
||||||
{
|
{
|
||||||
error = $"mubuf-extra-read-failed pc=0x{pc:X}";
|
error = $"mubuf-extra-read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -427,7 +383,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
return DecodeMubuf(word, mubufExtra, out name, out sizeDwords, out error);
|
return DecodeMubuf(word, mubufExtra, out name, out sizeDwords, out error);
|
||||||
case 0x3A:
|
case 0x3A:
|
||||||
encoding = Gen5ShaderEncoding.Mtbuf;
|
encoding = Gen5ShaderEncoding.Mtbuf;
|
||||||
if (!ctx.TryReadUInt32(baseAddress + pc + sizeof(uint), out var mtbufExtra))
|
if (!TryReadUInt32(ctx, baseAddress + pc + sizeof(uint), out var mtbufExtra))
|
||||||
{
|
{
|
||||||
error = $"mtbuf-extra-read-failed pc=0x{pc:X}";
|
error = $"mtbuf-extra-read-failed pc=0x{pc:X}";
|
||||||
return false;
|
return false;
|
||||||
@@ -1741,6 +1697,19 @@ internal static class Gen5ShaderTranslator
|
|||||||
counts[key] = count + 1;
|
counts[key] = count + 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private readonly record struct ShaderDecodeInfo(
|
private readonly record struct ShaderDecodeInfo(
|
||||||
int InstructionCount,
|
int InstructionCount,
|
||||||
Dictionary<string, int> Counts,
|
Dictionary<string, int> Counts,
|
||||||
|
|||||||
@@ -467,6 +467,9 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
_imageBindingByPc.TryAdd(binding.Pc, index);
|
_imageBindingByPc.TryAdd(binding.Pc, index);
|
||||||
var isStorage =
|
var isStorage =
|
||||||
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
|
Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
|
||||||
|
var isDepth =
|
||||||
|
binding.Opcode.Contains("SampleC", StringComparison.Ordinal) ||
|
||||||
|
binding.Opcode.Contains("Gather4C", StringComparison.Ordinal);
|
||||||
var (format, componentKind) =
|
var (format, componentKind) =
|
||||||
DecodeImageFormat(binding.ResourceDescriptor);
|
DecodeImageFormat(binding.ResourceDescriptor);
|
||||||
var componentType = componentKind switch
|
var componentType = componentKind switch
|
||||||
@@ -491,7 +494,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var imageType = _module.TypeImage(
|
var imageType = _module.TypeImage(
|
||||||
componentType,
|
componentType,
|
||||||
SpirvImageDim.Dim2D,
|
SpirvImageDim.Dim2D,
|
||||||
depth: false,
|
depth: isDepth,
|
||||||
arrayed: false,
|
arrayed: false,
|
||||||
multisampled: false,
|
multisampled: false,
|
||||||
sampled: isStorage ? 2u : 1u,
|
sampled: isStorage ? 2u : 1u,
|
||||||
@@ -1658,14 +1661,18 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
||||||
var imageOperands =
|
var imageOperands =
|
||||||
(explicitLod ? 2u : 0u) | (hasOffset ? 0x10u : 0u);
|
(explicitLod ? 2u : 0u) | (hasOffset ? 0x10u : 0u);
|
||||||
var reference = hasCompare
|
|
||||||
? Bitcast(_floatType, LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))
|
|
||||||
: 0u;
|
|
||||||
var operands = new List<uint>
|
var operands = new List<uint>
|
||||||
{
|
{
|
||||||
imageObject,
|
imageObject,
|
||||||
coordinates,
|
coordinates,
|
||||||
};
|
};
|
||||||
|
if (hasCompare)
|
||||||
|
{
|
||||||
|
operands.Add(
|
||||||
|
Bitcast(
|
||||||
|
_floatType,
|
||||||
|
LoadV(image.GetAddressRegister(hasOffset ? 1 : 0))));
|
||||||
|
}
|
||||||
|
|
||||||
if (imageOperands != 0)
|
if (imageOperands != 0)
|
||||||
{
|
{
|
||||||
@@ -1682,14 +1689,29 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
sampled = _module.AddInstruction(
|
sampled = _module.AddInstruction(
|
||||||
explicitLod
|
hasCompare
|
||||||
? SpirvOp.ImageSampleExplicitLod
|
? explicitLod
|
||||||
: SpirvOp.ImageSampleImplicitLod,
|
? SpirvOp.ImageSampleDrefExplicitLod
|
||||||
resource.VectorType,
|
: SpirvOp.ImageSampleDrefImplicitLod
|
||||||
|
: explicitLod
|
||||||
|
? SpirvOp.ImageSampleExplicitLod
|
||||||
|
: SpirvOp.ImageSampleImplicitLod,
|
||||||
|
hasCompare ? resource.ComponentType : resource.VectorType,
|
||||||
[.. operands]);
|
[.. operands]);
|
||||||
if (hasCompare)
|
if (hasCompare)
|
||||||
{
|
{
|
||||||
sampled = EmitManualDepthCompare(resource, sampled, reference);
|
var scalar = sampled;
|
||||||
|
sampled = _module.AddInstruction(
|
||||||
|
SpirvOp.CompositeConstruct,
|
||||||
|
resource.VectorType,
|
||||||
|
scalar,
|
||||||
|
scalar,
|
||||||
|
scalar,
|
||||||
|
resource.ComponentKind == ImageComponentKind.Float
|
||||||
|
? Float(1)
|
||||||
|
: resource.ComponentKind == ImageComponentKind.Uint
|
||||||
|
? UInt(1)
|
||||||
|
: _module.Constant(_intType, 1));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (instruction.Opcode.StartsWith(
|
else if (instruction.Opcode.StartsWith(
|
||||||
@@ -1703,9 +1725,6 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
var start = (hasOffset ? 1 : 0) + (hasCompare ? 1 : 0);
|
var start = (hasOffset ? 1 : 0) + (hasCompare ? 1 : 0);
|
||||||
var coordinates = BuildFloatCoordinates(image, start);
|
var coordinates = BuildFloatCoordinates(image, start);
|
||||||
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
var offset = hasOffset ? BuildImageOffset(image, 0) : 0u;
|
||||||
var reference = hasCompare
|
|
||||||
? Bitcast(_floatType, LoadV(image.GetAddressRegister(hasOffset ? 1 : 0)))
|
|
||||||
: 0u;
|
|
||||||
var operands = new List<uint>
|
var operands = new List<uint>
|
||||||
{
|
{
|
||||||
imageObject,
|
imageObject,
|
||||||
@@ -1713,7 +1732,10 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
};
|
};
|
||||||
if (hasCompare)
|
if (hasCompare)
|
||||||
{
|
{
|
||||||
operands.Add(UInt(0));
|
operands.Add(
|
||||||
|
Bitcast(
|
||||||
|
_floatType,
|
||||||
|
LoadV(image.GetAddressRegister(hasOffset ? 1 : 0))));
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1734,28 +1756,11 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
sampled = _module.AddInstruction(
|
sampled = _module.AddInstruction(
|
||||||
SpirvOp.ImageGather,
|
hasCompare
|
||||||
|
? SpirvOp.ImageDrefGather
|
||||||
|
: SpirvOp.ImageGather,
|
||||||
resource.VectorType,
|
resource.VectorType,
|
||||||
[.. operands]);
|
[.. operands]);
|
||||||
if (hasCompare)
|
|
||||||
{
|
|
||||||
var compared = new uint[4];
|
|
||||||
for (var component = 0u; component < 4; component++)
|
|
||||||
{
|
|
||||||
var texel = _module.AddInstruction(
|
|
||||||
SpirvOp.CompositeExtract,
|
|
||||||
resource.ComponentType,
|
|
||||||
sampled,
|
|
||||||
component);
|
|
||||||
compared[component] = EmitDepthCompareScalar(resource, texel, reference);
|
|
||||||
}
|
|
||||||
|
|
||||||
sampled = _module.AddInstruction(
|
|
||||||
SpirvOp.CompositeConstruct,
|
|
||||||
resource.VectorType,
|
|
||||||
compared);
|
|
||||||
}
|
|
||||||
|
|
||||||
writeAllComponents = true;
|
writeAllComponents = true;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -1789,67 +1794,6 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private uint EmitDepthCompareScalar(
|
|
||||||
SpirvImageResource resource,
|
|
||||||
uint texel,
|
|
||||||
uint reference)
|
|
||||||
{
|
|
||||||
var texelAsFloat = resource.ComponentKind switch
|
|
||||||
{
|
|
||||||
ImageComponentKind.Uint => _module.AddInstruction(
|
|
||||||
SpirvOp.ConvertUToF, _floatType, texel),
|
|
||||||
ImageComponentKind.Sint => _module.AddInstruction(
|
|
||||||
SpirvOp.ConvertSToF, _floatType, texel),
|
|
||||||
_ => texel,
|
|
||||||
};
|
|
||||||
var passes = _module.AddInstruction(
|
|
||||||
SpirvOp.FOrdLessThanEqual,
|
|
||||||
_boolType,
|
|
||||||
reference,
|
|
||||||
texelAsFloat);
|
|
||||||
return _module.AddInstruction(
|
|
||||||
SpirvOp.Select,
|
|
||||||
resource.ComponentType,
|
|
||||||
passes,
|
|
||||||
resource.ComponentKind switch
|
|
||||||
{
|
|
||||||
ImageComponentKind.Uint => UInt(1),
|
|
||||||
ImageComponentKind.Sint => _module.Constant(_intType, 1),
|
|
||||||
_ => Float(1),
|
|
||||||
},
|
|
||||||
resource.ComponentKind switch
|
|
||||||
{
|
|
||||||
ImageComponentKind.Uint => UInt(0),
|
|
||||||
ImageComponentKind.Sint => _module.Constant(_intType, 0),
|
|
||||||
_ => Float(0),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private uint EmitManualDepthCompare(
|
|
||||||
SpirvImageResource resource,
|
|
||||||
uint sampledVector,
|
|
||||||
uint reference)
|
|
||||||
{
|
|
||||||
var texel = _module.AddInstruction(
|
|
||||||
SpirvOp.CompositeExtract,
|
|
||||||
resource.ComponentType,
|
|
||||||
sampledVector,
|
|
||||||
0u);
|
|
||||||
var scalar = EmitDepthCompareScalar(resource, texel, reference);
|
|
||||||
return _module.AddInstruction(
|
|
||||||
SpirvOp.CompositeConstruct,
|
|
||||||
resource.VectorType,
|
|
||||||
scalar,
|
|
||||||
scalar,
|
|
||||||
scalar,
|
|
||||||
resource.ComponentKind switch
|
|
||||||
{
|
|
||||||
ImageComponentKind.Uint => UInt(1),
|
|
||||||
ImageComponentKind.Sint => _module.Constant(_intType, 1),
|
|
||||||
_ => Float(1),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private uint BuildFloatCoordinates(Gen5ImageControl image, int start)
|
private uint BuildFloatCoordinates(Gen5ImageControl image, int start)
|
||||||
{
|
{
|
||||||
var x = Bitcast(
|
var x = Bitcast(
|
||||||
|
|||||||
@@ -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,
|
|
||||||
};
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -24,7 +24,6 @@ public static class AmprExports
|
|||||||
private const uint KernelEventQueueRecordType = 2;
|
private const uint KernelEventQueueRecordType = 2;
|
||||||
private const uint WriteAddressRecordType = 3;
|
private const uint WriteAddressRecordType = 3;
|
||||||
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
||||||
private static readonly ConcurrentDictionary<string, Lazy<CachedHostFile>> _hostFileCache = new(StringComparer.OrdinalIgnoreCase);
|
|
||||||
private static readonly bool _traceAmpr =
|
private static readonly bool _traceAmpr =
|
||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||||
private static readonly bool _traceAmprReads =
|
private static readonly bool _traceAmprReads =
|
||||||
@@ -38,23 +37,6 @@ public static class AmprExports
|
|||||||
public ulong WriteOffset;
|
public ulong WriteOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class CachedHostFile
|
|
||||||
{
|
|
||||||
public CachedHostFile(string path)
|
|
||||||
{
|
|
||||||
Stream = new FileStream(
|
|
||||||
path,
|
|
||||||
FileMode.Open,
|
|
||||||
FileAccess.Read,
|
|
||||||
FileShare.ReadWrite | FileShare.Delete,
|
|
||||||
bufferSize: 1024 * 1024,
|
|
||||||
FileOptions.RandomAccess);
|
|
||||||
}
|
|
||||||
|
|
||||||
public object Gate { get; } = new();
|
|
||||||
public FileStream Stream { get; }
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "8aI7R7WaOlc",
|
Nid = "8aI7R7WaOlc",
|
||||||
ExportName = "sceAmprCommandBufferConstructor",
|
ExportName = "sceAmprCommandBufferConstructor",
|
||||||
@@ -267,53 +249,19 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
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) || !File.Exists(hostPath))
|
||||||
{
|
{
|
||||||
if (destination != 0 && size > 0)
|
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;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Offset -1 means "continue after the previous read of this file id".
|
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
|
||||||
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);
|
|
||||||
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
|
|
||||||
|
|
||||||
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
@@ -487,7 +435,7 @@ public static class AmprExports
|
|||||||
var offset = 0UL;
|
var offset = 0UL;
|
||||||
while (offset < writeOffset)
|
while (offset < writeOffset)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
|
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -686,43 +634,24 @@ public static class AmprExports
|
|||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!TryGetCachedHostFile(hostPath, out var cachedFile, out var openResult))
|
using var stream = new FileStream(
|
||||||
{
|
hostPath,
|
||||||
return openResult;
|
FileMode.Open,
|
||||||
}
|
FileAccess.Read,
|
||||||
|
FileShare.ReadWrite | FileShare.Delete,
|
||||||
long fileLength;
|
ChunkSize,
|
||||||
lock (cachedFile.Gate)
|
FileOptions.SequentialScan);
|
||||||
{
|
if (fileOffset >= (ulong)stream.Length)
|
||||||
fileLength = cachedFile.Stream.Length;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fileOffset >= (ulong)fileLength)
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
stream.Position = unchecked((long)fileOffset);
|
||||||
|
|
||||||
while (bytesRead < size)
|
while (bytesRead < size)
|
||||||
{
|
{
|
||||||
if (bytesRead > ulong.MaxValue - fileOffset)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var absoluteOffset = fileOffset + bytesRead;
|
|
||||||
if (absoluteOffset > long.MaxValue)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
||||||
int read;
|
var read = stream.Read(buffer, 0, request);
|
||||||
lock (cachedFile.Gate)
|
|
||||||
{
|
|
||||||
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
|
|
||||||
read = cachedFile.Stream.Read(buffer, 0, request);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (read <= 0)
|
if (read <= 0)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -752,44 +681,6 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGetCachedHostFile(string hostPath, out CachedHostFile file, out int result)
|
|
||||||
{
|
|
||||||
file = null!;
|
|
||||||
result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
|
|
||||||
string cachePath;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
cachePath = Path.GetFullPath(hostPath);
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
cachePath = hostPath;
|
|
||||||
}
|
|
||||||
|
|
||||||
var lazy = _hostFileCache.GetOrAdd(
|
|
||||||
cachePath,
|
|
||||||
static path => new Lazy<CachedHostFile>(() => new CachedHostFile(path), isThreadSafe: true));
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
file = lazy.Value;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch (UnauthorizedAccessException)
|
|
||||||
{
|
|
||||||
_hostFileCache.TryRemove(cachePath, out _);
|
|
||||||
result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
catch (IOException)
|
|
||||||
{
|
|
||||||
_hostFileCache.TryRemove(cachePath, out _);
|
|
||||||
result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool AppendReadFileRecord(
|
private static bool AppendReadFileRecord(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong commandBuffer,
|
ulong commandBuffer,
|
||||||
@@ -917,6 +808,19 @@ public static class AmprExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
||||||
{
|
{
|
||||||
if (!_traceAmpr)
|
if (!_traceAmpr)
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ internal static class AmprFileRegistry
|
|||||||
return _hostPathsById.TryGetValue(id, out hostPath!);
|
return _hostPathsById.TryGetValue(id, out hostPath!);
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static uint ComputeFileId(string guestPath)
|
private static uint ComputeFileId(string guestPath)
|
||||||
{
|
{
|
||||||
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
|
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
|
||||||
|
|
||||||
|
|||||||
@@ -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];
|
var valueAddress = ctx[CpuRegister.Rsi];
|
||||||
if (valueAddress == 0)
|
if (valueAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
int value;
|
int value;
|
||||||
@@ -83,18 +83,18 @@ public static class AppContentExports
|
|||||||
}
|
}
|
||||||
else if (!TryReadUserDefinedParam(paramId, out value))
|
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)];
|
Span<byte> valueBytes = stackalloc byte[sizeof(int)];
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||||
if (!ctx.Memory.TryWrite(valueAddress, valueBytes))
|
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}");
|
TraceAppContent($"app_param_get_int id={paramId} value={value}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[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)
|
private static void TraceAppContent(string message)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -3,19 +3,15 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Audio;
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
public static class AudioOut2Exports
|
public static class AudioOut2Exports
|
||||||
{
|
{
|
||||||
// Sized from guest evidence, not SDK headers: Quake keeps its
|
private const int AudioOut2ContextParamSize = 0x80;
|
||||||
// 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 AudioOut2ContextMemorySize = 0x10000;
|
private const int AudioOut2ContextMemorySize = 0x10000;
|
||||||
|
private const int AudioOut2ContextMemoryAlignment = 0x10000;
|
||||||
private static long _nextContextHandle = 1;
|
private static long _nextContextHandle = 1;
|
||||||
private static long _nextUserHandle = 1;
|
private static long _nextUserHandle = 1;
|
||||||
private static int _nextPortId;
|
private static int _nextPortId;
|
||||||
@@ -41,7 +37,7 @@ public static class AudioOut2Exports
|
|||||||
var paramAddress = ctx[CpuRegister.Rdi];
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
if (paramAddress == 0)
|
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];
|
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||||
@@ -52,8 +48,8 @@ public static class AudioOut2Exports
|
|||||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(paramAddress, param)
|
return ctx.Memory.TryWrite(paramAddress, param)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -64,15 +60,22 @@ public static class AudioOut2Exports
|
|||||||
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var paramAddress = ctx[CpuRegister.Rdi];
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
|
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||||
if (paramAddress == 0 || outMemorySizeAddress == 0)
|
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)
|
Span<byte> memoryInfo = stackalloc byte[0x20];
|
||||||
? ctx.SetReturn(0)
|
memoryInfo.Clear();
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
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(
|
[SysAbiExport(
|
||||||
@@ -88,13 +91,13 @@ public static class AudioOut2Exports
|
|||||||
var outContextAddress = ctx[CpuRegister.Rcx];
|
var outContextAddress = ctx[CpuRegister.Rcx];
|
||||||
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
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);
|
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
return TryWriteUInt64(ctx, outContextAddress, handle)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -102,7 +105,7 @@ public static class AudioOut2Exports
|
|||||||
ExportName = "sceAudioOut2ContextDestroy",
|
ExportName = "sceAudioOut2ContextDestroy",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
public static int AudioOut2ContextDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "JK2wamZPzwM",
|
Nid = "JK2wamZPzwM",
|
||||||
@@ -117,14 +120,14 @@ public static class AudioOut2Exports
|
|||||||
var contextAddress = ctx[CpuRegister.Rcx];
|
var contextAddress = ctx[CpuRegister.Rcx];
|
||||||
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
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 portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
||||||
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
||||||
return ctx.TryWriteUInt64(outPortAddress, handle)
|
return TryWriteUInt64(ctx, outPortAddress, handle)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -138,7 +141,7 @@ public static class AudioOut2Exports
|
|||||||
var stateAddress = ctx[CpuRegister.Rsi];
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
if (handle == 0 || stateAddress == 0)
|
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);
|
var type = (int)((handle >> 16) & 0xFF);
|
||||||
@@ -151,8 +154,8 @@ public static class AudioOut2Exports
|
|||||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(stateAddress, state)
|
return ctx.Memory.TryWrite(stateAddress, state)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -165,7 +168,7 @@ public static class AudioOut2Exports
|
|||||||
var infoAddress = ctx[CpuRegister.Rdi];
|
var infoAddress = ctx[CpuRegister.Rdi];
|
||||||
if (infoAddress == 0)
|
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];
|
Span<byte> info = stackalloc byte[0x40];
|
||||||
@@ -175,8 +178,8 @@ public static class AudioOut2Exports
|
|||||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
||||||
|
|
||||||
return ctx.Memory.TryWrite(infoAddress, info)
|
return ctx.Memory.TryWrite(infoAddress, info)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -184,14 +187,14 @@ public static class AudioOut2Exports
|
|||||||
ExportName = "sceAudioOut2PortDestroy",
|
ExportName = "sceAudioOut2PortDestroy",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "IaZXJ9M79uo",
|
Nid = "IaZXJ9M79uo",
|
||||||
ExportName = "sceAudioOut2UserDestroy",
|
ExportName = "sceAudioOut2UserDestroy",
|
||||||
Target = Generation.Gen5,
|
Target = Generation.Gen5,
|
||||||
LibraryName = "libSceAudioOut2")]
|
LibraryName = "libSceAudioOut2")]
|
||||||
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
public static int AudioOut2UserDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "xywYcRB7nbQ",
|
Nid = "xywYcRB7nbQ",
|
||||||
@@ -204,12 +207,25 @@ public static class AudioOut2Exports
|
|||||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||||
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
|
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);
|
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||||
return ctx.TryWriteUInt64(outUserAddress, handle)
|
return TryWriteUInt64(ctx, outUserAddress, handle)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,234 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using System.Buffers;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Diagnostics;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Audio;
|
|
||||||
|
|
||||||
public static class AudioOutExports
|
|
||||||
{
|
|
||||||
private static readonly ConcurrentDictionary<int, PortState> Ports = new();
|
|
||||||
private static int _nextPortHandle;
|
|
||||||
|
|
||||||
private sealed class PortState : IDisposable
|
|
||||||
{
|
|
||||||
private readonly object _paceGate = new();
|
|
||||||
private long _nextSilentOutput;
|
|
||||||
|
|
||||||
public PortState(
|
|
||||||
int userId,
|
|
||||||
int type,
|
|
||||||
uint bufferLength,
|
|
||||||
uint frequency,
|
|
||||||
int format,
|
|
||||||
int channels,
|
|
||||||
int bytesPerSample,
|
|
||||||
bool isFloat,
|
|
||||||
WinMmAudioPort? backend)
|
|
||||||
{
|
|
||||||
UserId = userId;
|
|
||||||
Type = type;
|
|
||||||
BufferLength = bufferLength;
|
|
||||||
Frequency = frequency;
|
|
||||||
Format = format;
|
|
||||||
Channels = channels;
|
|
||||||
BytesPerSample = bytesPerSample;
|
|
||||||
IsFloat = isFloat;
|
|
||||||
Backend = backend;
|
|
||||||
}
|
|
||||||
|
|
||||||
public int UserId { get; }
|
|
||||||
public int Type { get; }
|
|
||||||
public uint BufferLength { get; }
|
|
||||||
public uint Frequency { get; }
|
|
||||||
public int Format { get; }
|
|
||||||
public int Channels { get; }
|
|
||||||
public int BytesPerSample { get; }
|
|
||||||
public bool IsFloat { get; }
|
|
||||||
public WinMmAudioPort? Backend { get; }
|
|
||||||
public int BufferByteLength =>
|
|
||||||
checked((int)BufferLength * Channels * BytesPerSample);
|
|
||||||
|
|
||||||
public void PaceSilence()
|
|
||||||
{
|
|
||||||
long delay;
|
|
||||||
lock (_paceGate)
|
|
||||||
{
|
|
||||||
var now = Stopwatch.GetTimestamp();
|
|
||||||
if (_nextSilentOutput < now)
|
|
||||||
{
|
|
||||||
_nextSilentOutput = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
delay = _nextSilentOutput - now;
|
|
||||||
_nextSilentOutput += checked(
|
|
||||||
(long)Math.Ceiling(
|
|
||||||
Stopwatch.Frequency * (double)BufferLength / Frequency));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (delay > 0)
|
|
||||||
{
|
|
||||||
Thread.Sleep(TimeSpan.FromSeconds((double)delay / Stopwatch.Frequency));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose() => Backend?.Dispose();
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "JfEPXVxhFqA",
|
|
||||||
ExportName = "sceAudioOutInit",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAudioOut")]
|
|
||||||
public static int AudioOutInit(CpuContext ctx) => ctx.SetReturn(0);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "ekNvsT22rsY",
|
|
||||||
ExportName = "sceAudioOutOpen",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAudioOut")]
|
|
||||||
public static int AudioOutOpen(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var type = unchecked((int)ctx[CpuRegister.Rsi]);
|
|
||||||
var bufferLength = unchecked((uint)ctx[CpuRegister.Rcx]);
|
|
||||||
var frequency = unchecked((uint)ctx[CpuRegister.R8]);
|
|
||||||
var format = unchecked((int)ctx[CpuRegister.R9]);
|
|
||||||
if (bufferLength == 0 || frequency == 0 ||
|
|
||||||
!TryGetFormat(format, out var channels, out var bytesPerSample, out var isFloat))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
WinMmAudioPort? backend = null;
|
|
||||||
string backendName;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
backend = new WinMmAudioPort(frequency);
|
|
||||||
backendName = "winmm";
|
|
||||||
}
|
|
||||||
catch (Exception exception)
|
|
||||||
{
|
|
||||||
backendName = "silent";
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] AudioOut host backend unavailable: {exception.Message}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var handle = Interlocked.Increment(ref _nextPortHandle);
|
|
||||||
Ports[handle] = new PortState(
|
|
||||||
userId,
|
|
||||||
type,
|
|
||||||
bufferLength,
|
|
||||||
frequency,
|
|
||||||
format,
|
|
||||||
channels,
|
|
||||||
bytesPerSample,
|
|
||||||
isFloat,
|
|
||||||
backend);
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][INFO] AudioOut port {handle}: {frequency} Hz, " +
|
|
||||||
$"{channels} ch, {(isFloat ? "float32" : "s16")}, " +
|
|
||||||
$"{bufferLength} frames, backend={backendName}");
|
|
||||||
return ctx.SetReturn(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "s1--uE9mBFw",
|
|
||||||
ExportName = "sceAudioOutClose",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAudioOut")]
|
|
||||||
public static int AudioOutClose(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
if (!Ports.TryRemove(handle, out var port))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
port.Dispose();
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "QOQtbeDqsT4",
|
|
||||||
ExportName = "sceAudioOutOutput",
|
|
||||||
Target = Generation.Gen5,
|
|
||||||
LibraryName = "libSceAudioOut")]
|
|
||||||
public static int AudioOutOutput(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var sourceAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (!Ports.TryGetValue(handle, out var port))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (sourceAddress == 0)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
|
|
||||||
var buffer = ArrayPool<byte>.Shared.Rent(port.BufferByteLength);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
var source = buffer.AsSpan(0, port.BufferByteLength);
|
|
||||||
if (!ctx.Memory.TryRead(sourceAddress, source))
|
|
||||||
{
|
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port.Backend is null ||
|
|
||||||
!port.Backend.Submit(
|
|
||||||
source,
|
|
||||||
port.BufferLength,
|
|
||||||
port.Channels,
|
|
||||||
port.BytesPerSample,
|
|
||||||
port.IsFloat))
|
|
||||||
{
|
|
||||||
port.PaceSilence();
|
|
||||||
}
|
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<byte>.Shared.Return(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "b+uAV89IlxE",
|
|
||||||
ExportName = "sceAudioOutSetVolume",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAudioOut")]
|
|
||||||
public static int AudioOutSetVolume(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
return ctx.SetReturn(
|
|
||||||
Ports.ContainsKey(handle)
|
|
||||||
? 0
|
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryGetFormat(
|
|
||||||
int rawFormat,
|
|
||||||
out int channels,
|
|
||||||
out int bytesPerSample,
|
|
||||||
out bool isFloat)
|
|
||||||
{
|
|
||||||
var format = rawFormat & 0xFF;
|
|
||||||
channels = format switch
|
|
||||||
{
|
|
||||||
0 or 3 => 1,
|
|
||||||
1 or 4 => 2,
|
|
||||||
2 or 5 or 6 or 7 => 8,
|
|
||||||
_ => 0,
|
|
||||||
};
|
|
||||||
bytesPerSample = format is >= 3 and <= 5 or 7 ? 4 : 2;
|
|
||||||
isFloat = bytesPerSample == 4;
|
|
||||||
return channels != 0;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,302 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using System.Buffers;
|
|
||||||
using System.Buffers.Binary;
|
|
||||||
using System.Runtime.InteropServices;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Audio;
|
|
||||||
|
|
||||||
internal sealed class WinMmAudioPort : IDisposable
|
|
||||||
{
|
|
||||||
private const uint WaveMapper = uint.MaxValue;
|
|
||||||
private const uint CallbackEvent = 0x0005_0000;
|
|
||||||
private const ushort WaveFormatPcm = 1;
|
|
||||||
private const uint WaveHeaderDone = 0x0000_0001;
|
|
||||||
private const int MaximumQueuedPcmBytes = 32 * 1024;
|
|
||||||
|
|
||||||
private readonly object _gate = new();
|
|
||||||
private readonly AutoResetEvent _completion = new(false);
|
|
||||||
private readonly Queue<NativeBuffer> _buffers = new();
|
|
||||||
private IntPtr _device;
|
|
||||||
private int _queuedPcmBytes;
|
|
||||||
private bool _disposed;
|
|
||||||
|
|
||||||
public WinMmAudioPort(uint sampleRate)
|
|
||||||
{
|
|
||||||
if (!OperatingSystem.IsWindows())
|
|
||||||
{
|
|
||||||
throw new PlatformNotSupportedException("WinMM audio is only available on Windows.");
|
|
||||||
}
|
|
||||||
|
|
||||||
var format = new WaveFormat
|
|
||||||
{
|
|
||||||
FormatTag = WaveFormatPcm,
|
|
||||||
Channels = 2,
|
|
||||||
SamplesPerSecond = sampleRate,
|
|
||||||
AverageBytesPerSecond = checked(sampleRate * 4),
|
|
||||||
BlockAlign = 4,
|
|
||||||
BitsPerSample = 16,
|
|
||||||
ExtraSize = 0,
|
|
||||||
};
|
|
||||||
var result = WaveOutOpen(
|
|
||||||
out _device,
|
|
||||||
WaveMapper,
|
|
||||||
ref format,
|
|
||||||
_completion.SafeWaitHandle.DangerousGetHandle(),
|
|
||||||
IntPtr.Zero,
|
|
||||||
CallbackEvent);
|
|
||||||
if (result != 0)
|
|
||||||
{
|
|
||||||
throw new InvalidOperationException($"waveOutOpen failed with MMRESULT {result}.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool Submit(
|
|
||||||
ReadOnlySpan<byte> source,
|
|
||||||
uint frames,
|
|
||||||
int channels,
|
|
||||||
int bytesPerSample,
|
|
||||||
bool isFloat)
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var outputLength = checked((int)frames * 4);
|
|
||||||
ReapCompletedBuffers();
|
|
||||||
while (_queuedPcmBytes != 0 &&
|
|
||||||
_queuedPcmBytes + outputLength > MaximumQueuedPcmBytes)
|
|
||||||
{
|
|
||||||
if (!_completion.WaitOne(TimeSpan.FromSeconds(1)))
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
ReapCompletedBuffers();
|
|
||||||
}
|
|
||||||
|
|
||||||
var output = ArrayPool<byte>.Shared.Rent(outputLength);
|
|
||||||
try
|
|
||||||
{
|
|
||||||
ConvertToStereoPcm16(
|
|
||||||
source,
|
|
||||||
output.AsSpan(0, outputLength),
|
|
||||||
checked((int)frames),
|
|
||||||
channels,
|
|
||||||
bytesPerSample,
|
|
||||||
isFloat);
|
|
||||||
return QueueBuffer(output.AsSpan(0, outputLength));
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
ArrayPool<byte>.Shared.Return(output);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public void Dispose()
|
|
||||||
{
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_disposed)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_disposed = true;
|
|
||||||
if (_device != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
WaveOutReset(_device);
|
|
||||||
while (_buffers.TryDequeue(out var buffer))
|
|
||||||
{
|
|
||||||
ReleaseBuffer(buffer);
|
|
||||||
}
|
|
||||||
|
|
||||||
WaveOutClose(_device);
|
|
||||||
_device = IntPtr.Zero;
|
|
||||||
}
|
|
||||||
|
|
||||||
_completion.Dispose();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private bool QueueBuffer(ReadOnlySpan<byte> data)
|
|
||||||
{
|
|
||||||
var dataAddress = Marshal.AllocHGlobal(data.Length);
|
|
||||||
var headerAddress = IntPtr.Zero;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
unsafe
|
|
||||||
{
|
|
||||||
data.CopyTo(new Span<byte>((void*)dataAddress, data.Length));
|
|
||||||
}
|
|
||||||
|
|
||||||
var header = new WaveHeader
|
|
||||||
{
|
|
||||||
Data = dataAddress,
|
|
||||||
BufferLength = checked((uint)data.Length),
|
|
||||||
};
|
|
||||||
headerAddress = Marshal.AllocHGlobal(Marshal.SizeOf<WaveHeader>());
|
|
||||||
Marshal.StructureToPtr(header, headerAddress, false);
|
|
||||||
|
|
||||||
var result = WaveOutPrepareHeader(
|
|
||||||
_device,
|
|
||||||
headerAddress,
|
|
||||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
|
||||||
if (result != 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
result = WaveOutWrite(
|
|
||||||
_device,
|
|
||||||
headerAddress,
|
|
||||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
|
||||||
if (result != 0)
|
|
||||||
{
|
|
||||||
WaveOutUnprepareHeader(
|
|
||||||
_device,
|
|
||||||
headerAddress,
|
|
||||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
_buffers.Enqueue(new NativeBuffer(dataAddress, headerAddress, data.Length));
|
|
||||||
_queuedPcmBytes += data.Length;
|
|
||||||
dataAddress = IntPtr.Zero;
|
|
||||||
headerAddress = IntPtr.Zero;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
if (headerAddress != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
Marshal.FreeHGlobal(headerAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (dataAddress != IntPtr.Zero)
|
|
||||||
{
|
|
||||||
Marshal.FreeHGlobal(dataAddress);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReapCompletedBuffers()
|
|
||||||
{
|
|
||||||
while (_buffers.TryPeek(out var buffer))
|
|
||||||
{
|
|
||||||
var header = Marshal.PtrToStructure<WaveHeader>(buffer.Header);
|
|
||||||
if ((header.Flags & WaveHeaderDone) == 0)
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
_buffers.Dequeue();
|
|
||||||
ReleaseBuffer(buffer);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private void ReleaseBuffer(NativeBuffer buffer)
|
|
||||||
{
|
|
||||||
WaveOutUnprepareHeader(
|
|
||||||
_device,
|
|
||||||
buffer.Header,
|
|
||||||
checked((uint)Marshal.SizeOf<WaveHeader>()));
|
|
||||||
_queuedPcmBytes -= buffer.Length;
|
|
||||||
Marshal.FreeHGlobal(buffer.Header);
|
|
||||||
Marshal.FreeHGlobal(buffer.Data);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ConvertToStereoPcm16(
|
|
||||||
ReadOnlySpan<byte> source,
|
|
||||||
Span<byte> destination,
|
|
||||||
int frames,
|
|
||||||
int channels,
|
|
||||||
int bytesPerSample,
|
|
||||||
bool isFloat)
|
|
||||||
{
|
|
||||||
var sourceFrameSize = checked(channels * bytesPerSample);
|
|
||||||
for (var frame = 0; frame < frames; frame++)
|
|
||||||
{
|
|
||||||
var sourceFrame = source.Slice(frame * sourceFrameSize, sourceFrameSize);
|
|
||||||
var left = ReadSample(sourceFrame, 0, bytesPerSample, isFloat);
|
|
||||||
var right = channels == 1
|
|
||||||
? left
|
|
||||||
: ReadSample(sourceFrame, 1, bytesPerSample, isFloat);
|
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(destination[(frame * 4)..], left);
|
|
||||||
BinaryPrimitives.WriteInt16LittleEndian(destination[((frame * 4) + 2)..], right);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static short ReadSample(
|
|
||||||
ReadOnlySpan<byte> frame,
|
|
||||||
int channel,
|
|
||||||
int bytesPerSample,
|
|
||||||
bool isFloat)
|
|
||||||
{
|
|
||||||
var sample = frame.Slice(channel * bytesPerSample, bytesPerSample);
|
|
||||||
if (!isFloat)
|
|
||||||
{
|
|
||||||
return BinaryPrimitives.ReadInt16LittleEndian(sample);
|
|
||||||
}
|
|
||||||
|
|
||||||
var bits = BinaryPrimitives.ReadInt32LittleEndian(sample);
|
|
||||||
var value = Math.Clamp(BitConverter.Int32BitsToSingle(bits), -1.0f, 1.0f);
|
|
||||||
return checked((short)MathF.Round(value * short.MaxValue));
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct NativeBuffer(IntPtr Data, IntPtr Header, int Length);
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential, Pack = 2)]
|
|
||||||
private struct WaveFormat
|
|
||||||
{
|
|
||||||
public ushort FormatTag;
|
|
||||||
public ushort Channels;
|
|
||||||
public uint SamplesPerSecond;
|
|
||||||
public uint AverageBytesPerSecond;
|
|
||||||
public ushort BlockAlign;
|
|
||||||
public ushort BitsPerSample;
|
|
||||||
public ushort ExtraSize;
|
|
||||||
}
|
|
||||||
|
|
||||||
[StructLayout(LayoutKind.Sequential)]
|
|
||||||
private struct WaveHeader
|
|
||||||
{
|
|
||||||
public IntPtr Data;
|
|
||||||
public uint BufferLength;
|
|
||||||
public uint BytesRecorded;
|
|
||||||
public nuint User;
|
|
||||||
public uint Flags;
|
|
||||||
public uint Loops;
|
|
||||||
public IntPtr Next;
|
|
||||||
public nuint Reserved;
|
|
||||||
}
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutOpen")]
|
|
||||||
private static extern uint WaveOutOpen(
|
|
||||||
out IntPtr device,
|
|
||||||
uint deviceId,
|
|
||||||
ref WaveFormat format,
|
|
||||||
IntPtr callback,
|
|
||||||
IntPtr instance,
|
|
||||||
uint flags);
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutPrepareHeader")]
|
|
||||||
private static extern uint WaveOutPrepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutWrite")]
|
|
||||||
private static extern uint WaveOutWrite(IntPtr device, IntPtr header, uint headerSize);
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutUnprepareHeader")]
|
|
||||||
private static extern uint WaveOutUnprepareHeader(IntPtr device, IntPtr header, uint headerSize);
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutReset")]
|
|
||||||
private static extern uint WaveOutReset(IntPtr device);
|
|
||||||
|
|
||||||
[DllImport("winmm.dll", EntryPoint = "waveOutClose")]
|
|
||||||
private static extern uint WaveOutClose(IntPtr device);
|
|
||||||
}
|
|
||||||
@@ -1,104 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using SharpEmu.Libs.Kernel;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.AvPlayer;
|
|
||||||
|
|
||||||
public static class AvPlayerExports
|
|
||||||
{
|
|
||||||
private const int InvalidParameters = unchecked((int)0x806A0001);
|
|
||||||
private static readonly object StateGate = new();
|
|
||||||
private static readonly HashSet<ulong> Players = new();
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "aS66RI0gGgo",
|
|
||||||
ExportName = "sceAvPlayerInit",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAvPlayer")]
|
|
||||||
public static int AvPlayerInit(CpuContext ctx)
|
|
||||||
{
|
|
||||||
if (ctx[CpuRegister.Rdi] == 0 ||
|
|
||||||
!KernelMemoryCompatExports.TryAllocateHleData(ctx, 0x40, 16, out var handle))
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return 0;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
Players.Add(handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = handle;
|
|
||||||
return unchecked((int)handle);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "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",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAvPlayer")]
|
|
||||||
public static int AvPlayerPostInit(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var handle = ctx[CpuRegister.Rdi];
|
|
||||||
var dataAddress = ctx[CpuRegister.Rsi];
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(
|
|
||||||
handle != 0 && dataAddress != 0 && Players.Contains(handle)
|
|
||||||
? 0
|
|
||||||
: InvalidParameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "NkJwDzKmIlw",
|
|
||||||
ExportName = "sceAvPlayerClose",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libSceAvPlayer")]
|
|
||||||
public static int AvPlayerClose(CpuContext ctx)
|
|
||||||
{
|
|
||||||
lock (StateGate)
|
|
||||||
{
|
|
||||||
return ctx.SetReturn(Players.Remove(ctx[CpuRegister.Rdi]) ? 0 : InvalidParameters);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,6 +8,7 @@ namespace SharpEmu.Libs.CommonDialog;
|
|||||||
|
|
||||||
public static class CommonDialogExports
|
public static class CommonDialogExports
|
||||||
{
|
{
|
||||||
|
private const int AlreadySystemInitialized = unchecked((int)0x80B80002);
|
||||||
private static int _initialized;
|
private static int _initialized;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -17,12 +18,11 @@ public static class CommonDialogExports
|
|||||||
LibraryName = "libSceCommonDialog")]
|
LibraryName = "libSceCommonDialog")]
|
||||||
public static int CommonDialogInitialize(CpuContext ctx)
|
public static int CommonDialogInitialize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
// Games can initialize the common-dialog service defensively from more than one
|
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
||||||
// subsystem. Keeping this HLE initialization idempotent avoids turning an error
|
? 0
|
||||||
// reporting path into a second, unrelated failure.
|
: AlreadySystemInitialized;
|
||||||
Interlocked.Exchange(ref _initialized, 1);
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
return result;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|||||||
@@ -1,185 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using System.Threading;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.CommonDialog;
|
|
||||||
|
|
||||||
public static class MsgDialogExports
|
|
||||||
{
|
|
||||||
private const int StatusNone = 0;
|
|
||||||
private const int StatusInitialized = 1;
|
|
||||||
private const int StatusFinished = 3;
|
|
||||||
private const int ResultSize = 0x20;
|
|
||||||
|
|
||||||
private static int _initialized;
|
|
||||||
private static int _status;
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "lDqxaY1UbEo",
|
|
||||||
ExportName = "sceMsgDialogInitialize",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
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)
|
|
||||||
{
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -93,7 +93,7 @@ public static class CxaGuardExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "9rAeANT2tyE",
|
Nid = "",
|
||||||
ExportName = "__cxa_guard_release",
|
ExportName = "__cxa_guard_release",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libc")]
|
LibraryName = "libc")]
|
||||||
@@ -142,7 +142,7 @@ public static class CxaGuardExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "2emaaluWzUw",
|
Nid = "",
|
||||||
ExportName = "__cxa_guard_abort",
|
ExportName = "__cxa_guard_abort",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libc")]
|
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];
|
var optParam = ctx[CpuRegister.Rdi];
|
||||||
if (optParam == 0)
|
if (optParam == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((optParam & 7) != 0)
|
if ((optParam & 7) != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorAlignment);
|
return SetReturn(ctx, FiberErrorAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -127,24 +127,24 @@ public static class FiberExports
|
|||||||
var fiber = ctx[CpuRegister.Rdi];
|
var fiber = ctx[CpuRegister.Rdi];
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
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)
|
if (state != FiberStateIdle)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
_continuations.TryRemove(fiber, out _);
|
_continuations.TryRemove(fiber, out _);
|
||||||
_returnTargets.TryRemove(fiber, out _);
|
_returnTargets.TryRemove(fiber, out _);
|
||||||
_stackRanges.TryRemove(fiber, out _);
|
_stackRanges.TryRemove(fiber, out _);
|
||||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -229,20 +229,20 @@ public static class FiberExports
|
|||||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||||
if (fiberAddress == 0)
|
if (fiberAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
var returnArgument = ctx[CpuRegister.Rdi];
|
var returnArgument = ctx[CpuRegister.Rdi];
|
||||||
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
||||||
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
|
if (argOnRunAddress != 0 && !TryWriteUInt64(ctx, argOnRunAddress, 0))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestCpuContinuation transferTarget;
|
GuestCpuContinuation transferTarget;
|
||||||
@@ -256,7 +256,7 @@ public static class FiberExports
|
|||||||
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousFiber = returnTarget.PreviousFiber;
|
previousFiber = returnTarget.PreviousFiber;
|
||||||
@@ -264,10 +264,10 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
||||||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
||||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun))
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
transferTarget = previousContinuation.Context with { Rax = 0 };
|
transferTarget = previousContinuation.Context with { Rax = 0 };
|
||||||
@@ -278,15 +278,15 @@ public static class FiberExports
|
|||||||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(fiberAddress, out _);
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -296,7 +296,7 @@ public static class FiberExports
|
|||||||
TraceFiber(
|
TraceFiber(
|
||||||
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
||||||
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -309,18 +309,18 @@ public static class FiberExports
|
|||||||
var outAddress = ctx[CpuRegister.Rdi];
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||||
if (fiberAddress == 0)
|
if (fiberAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
return TryWriteUInt64(ctx, outAddress, fiberAddress)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -334,35 +334,35 @@ public static class FiberExports
|
|||||||
var info = ctx[CpuRegister.Rsi];
|
var info = ctx[CpuRegister.Rsi];
|
||||||
if (info == 0)
|
if (info == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
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))
|
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
|
||||||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
|
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
|
||||||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
|
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
|
||||||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
|
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
|
||||||
!TryWriteName(ctx, info + 40, fields.Name) ||
|
!TryWriteName(ctx, info + 40, fields.Name) ||
|
||||||
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.SetReturn(0);
|
return SetReturn(ctx, 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -376,22 +376,22 @@ public static class FiberExports
|
|||||||
var nameAddress = ctx[CpuRegister.Rsi];
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(error);
|
return SetReturn(ctx, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameAddress == 0)
|
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)
|
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -403,12 +403,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (ctx[CpuRegister.Rdi] != 0)
|
if (ctx[CpuRegister.Rdi] != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorState);
|
: SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -419,8 +419,8 @@ public static class FiberExports
|
|||||||
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
||||||
{
|
{
|
||||||
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorState);
|
: SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -433,17 +433,17 @@ public static class FiberExports
|
|||||||
var outAddress = ctx[CpuRegister.Rdi];
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ResolveCurrentFiberAddress(ctx) == 0)
|
if (ResolveCurrentFiberAddress(ctx) == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
return TryWriteUInt64(ctx, outAddress, ctx[CpuRegister.Rbp])
|
||||||
? ctx.SetReturn(0)
|
? SetReturn(ctx, 0)
|
||||||
: ctx.SetReturn(FiberErrorInvalid);
|
: SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FiberInitializeCore(
|
private static int FiberInitializeCore(
|
||||||
@@ -459,37 +459,37 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorNull);
|
return SetReturn(ctx, FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((fiber & 7) != 0 ||
|
if ((fiber & 7) != 0 ||
|
||||||
(contextAddress & 15) != 0 ||
|
(contextAddress & 15) != 0 ||
|
||||||
(optParam & 7) != 0)
|
(optParam & 7) != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorAlignment);
|
return SetReturn(ctx, FiberErrorAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorRange);
|
return SetReturn(ctx, FiberErrorRange);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((contextSize & 15) != 0 ||
|
if ((contextSize & 15) != 0 ||
|
||||||
(contextAddress == 0 && contextSize != 0) ||
|
(contextAddress == 0 && contextSize != 0) ||
|
||||||
(contextAddress != 0 && contextSize == 0))
|
(contextAddress != 0 && contextSize == 0))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optParam != 0 &&
|
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)
|
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
||||||
@@ -497,27 +497,27 @@ public static class FiberExports
|
|||||||
flags |= FiberFlagContextSizeCheck;
|
flags |= FiberFlagContextSizeCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
|
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
|
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
|
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
|
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextAddress != 0)
|
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)
|
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}'");
|
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(
|
private static int FiberRunCore(
|
||||||
@@ -547,12 +547,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(error);
|
return SetReturn(ctx, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachContextAddress != 0 || attachContextSize != 0)
|
if (attachContextAddress != 0 || attachContextSize != 0)
|
||||||
@@ -560,7 +560,7 @@ public static class FiberExports
|
|||||||
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
||||||
if (attachResult != 0)
|
if (attachResult != 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(attachResult);
|
return SetReturn(ctx, attachResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -568,16 +568,16 @@ public static class FiberExports
|
|||||||
if ((isSwitch && previousFiber == 0) ||
|
if ((isSwitch && previousFiber == 0) ||
|
||||||
(!isSwitch && previousFiber != 0))
|
(!isSwitch && previousFiber != 0))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
if (previousFiber == fiber)
|
if (previousFiber == fiber)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorPermission);
|
return SetReturn(ctx, FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestCpuContinuation transferTarget;
|
GuestCpuContinuation transferTarget;
|
||||||
@@ -586,12 +586,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (!TryReadFiberFields(ctx, fiber, out fields))
|
if (!TryReadFiberFields(ctx, fiber, out fields))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
if (fields.State != FiberStateIdle)
|
if (fields.State != FiberStateIdle)
|
||||||
{
|
{
|
||||||
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
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;
|
FiberContinuation targetContinuation;
|
||||||
@@ -602,12 +602,12 @@ public static class FiberExports
|
|||||||
}
|
}
|
||||||
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
var callerContinuation = new FiberContinuation(
|
var callerContinuation = new FiberContinuation(
|
||||||
@@ -616,11 +616,11 @@ public static class FiberExports
|
|||||||
|
|
||||||
if (previousFiber != 0)
|
if (previousFiber != 0)
|
||||||
{
|
{
|
||||||
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
|
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
|
||||||
previousState != FiberStateRun ||
|
previousState != FiberStateRun ||
|
||||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(FiberErrorState);
|
return SetReturn(ctx, FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
_continuations[previousFiber] = callerContinuation;
|
_continuations[previousFiber] = callerContinuation;
|
||||||
@@ -631,15 +631,15 @@ public static class FiberExports
|
|||||||
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
|
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
|
||||||
{
|
{
|
||||||
if (previousFiber != 0)
|
if (previousFiber != 0)
|
||||||
{
|
{
|
||||||
_continuations.TryRemove(previousFiber, out _);
|
_continuations.TryRemove(previousFiber, out _);
|
||||||
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
|
||||||
}
|
}
|
||||||
_returnTargets.TryRemove(fiber, out _);
|
_returnTargets.TryRemove(fiber, out _);
|
||||||
return ctx.SetReturn(FiberErrorInvalid);
|
return SetReturn(ctx, FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (resumed)
|
if (resumed)
|
||||||
@@ -656,7 +656,7 @@ public static class FiberExports
|
|||||||
TraceFiber(
|
TraceFiber(
|
||||||
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
$"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}");
|
$"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(
|
private static bool TryCreateInitialContinuation(
|
||||||
@@ -673,7 +673,7 @@ public static class FiberExports
|
|||||||
|
|
||||||
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
||||||
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
||||||
if (!ctx.TryWriteUInt64(entryRsp, 0))
|
if (!TryWriteUInt64(ctx, entryRsp, 0))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -708,7 +708,7 @@ public static class FiberExports
|
|||||||
FiberContinuation continuation,
|
FiberContinuation continuation,
|
||||||
ulong argument) =>
|
ulong argument) =>
|
||||||
continuation.ArgOnRunAddress == 0 ||
|
continuation.ArgOnRunAddress == 0 ||
|
||||||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
|
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
|
||||||
|
|
||||||
private static GuestCpuContinuation CaptureContinuation(
|
private static GuestCpuContinuation CaptureContinuation(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
@@ -779,11 +779,11 @@ public static class FiberExports
|
|||||||
return FiberErrorInvalid;
|
return FiberErrorInvalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||||
{
|
{
|
||||||
return FiberErrorInvalid;
|
return FiberErrorInvalid;
|
||||||
}
|
}
|
||||||
@@ -839,8 +839,8 @@ public static class FiberExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
|
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
|
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||||
magicStart != FiberSignature0 ||
|
magicStart != FiberSignature0 ||
|
||||||
magicEnd != FiberSignature1)
|
magicEnd != FiberSignature1)
|
||||||
{
|
{
|
||||||
@@ -855,12 +855,12 @@ public static class FiberExports
|
|||||||
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
||||||
{
|
{
|
||||||
fields = default;
|
fields = default;
|
||||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
|
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
|
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
|
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||||
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
|
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||||
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
|
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
|
||||||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -898,6 +898,46 @@ public static class FiberExports
|
|||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
||||||
@@ -925,6 +965,37 @@ public static class FiberExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||||
|
{
|
||||||
|
var bytes = new byte[capacity];
|
||||||
|
for (var index = 0; index < bytes.Length; index++)
|
||||||
|
{
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[index] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, int result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceFiber(string message)
|
private static void TraceFiber(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
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.HLE;
|
||||||
using SharpEmu.Libs.Ampr;
|
using SharpEmu.Libs.Ampr;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
|
|
||||||
@@ -49,7 +50,7 @@ public static class KernelAprCompatExports
|
|||||||
return completionResult;
|
return completionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -140,7 +141,7 @@ public static class KernelAprCompatExports
|
|||||||
return completionResult;
|
return completionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -149,19 +150,6 @@ public static class KernelAprCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
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)
|
private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress)
|
||||||
{
|
{
|
||||||
Span<byte> result = stackalloc byte[sizeof(ulong)];
|
Span<byte> result = stackalloc byte[sizeof(ulong)];
|
||||||
@@ -190,6 +178,13 @@ public static class KernelAprCompatExports
|
|||||||
return tag is 0x0C or 0x10;
|
return tag is 0x0C or 0x10;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceApr(
|
private static void TraceApr(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
string operation,
|
string operation,
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
@@ -51,17 +52,17 @@ public static class KernelEventFlagCompatExports
|
|||||||
optionAddress != 0 ||
|
optionAddress != 0 ||
|
||||||
!IsValidAttributes(attributes))
|
!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)
|
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));
|
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
||||||
@@ -75,11 +76,11 @@ public static class KernelEventFlagCompatExports
|
|||||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||||
{
|
{
|
||||||
_eventFlags.TryRemove(handle, out _);
|
_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}");
|
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(
|
[SysAbiExport(
|
||||||
@@ -92,7 +93,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var handle = ctx[CpuRegister.Rdi];
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
if (!_eventFlags.TryRemove(handle, out var state))
|
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)
|
lock (state.Gate)
|
||||||
@@ -101,7 +102,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -116,7 +117,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var returnRip = GetCurrentReturnRip();
|
var returnRip = GetCurrentReturnRip();
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
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)
|
lock (state.Gate)
|
||||||
@@ -127,7 +128,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -141,7 +142,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var pattern = ctx[CpuRegister.Rsi];
|
var pattern = ctx[CpuRegister.Rsi];
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
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)
|
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}");
|
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(
|
[SysAbiExport(
|
||||||
@@ -167,29 +168,29 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
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))
|
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)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
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))
|
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);
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
|
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))
|
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))
|
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;
|
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);
|
Monitor.Enter(state.Gate);
|
||||||
@@ -228,15 +229,15 @@ public static class KernelEventFlagCompatExports
|
|||||||
{
|
{
|
||||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(immediateWaitResult);
|
return SetReturn(ctx, immediateWaitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
@@ -271,7 +272,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var scheduler = GuestThreadExecution.Scheduler;
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
if (scheduler is null)
|
if (scheduler is null)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.WaitingThreads++;
|
state.WaitingThreads++;
|
||||||
@@ -296,7 +297,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
releaseWaiter = false;
|
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}");
|
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);
|
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
||||||
@@ -313,7 +314,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
state.WaitingThreads++;
|
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}");
|
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
|
finally
|
||||||
{
|
{
|
||||||
@@ -333,15 +334,15 @@ public static class KernelEventFlagCompatExports
|
|||||||
var waiterCountAddress = ctx[CpuRegister.Rdx];
|
var waiterCountAddress = ctx[CpuRegister.Rdx];
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
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)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
if (waiterCountAddress != 0 &&
|
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;
|
state.Bits = setPattern;
|
||||||
@@ -350,7 +351,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
TraceEventFlag($"cancel handle=0x{handle:X16} bits=0x{setPattern:X16}");
|
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)
|
private static bool IsValidAttributes(uint attributes)
|
||||||
@@ -452,6 +453,84 @@ public static class KernelEventFlagCompatExports
|
|||||||
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
||||||
address == 0 || ctx.TryWriteUInt64(address, bits);
|
address == 0 || ctx.TryWriteUInt64(address, bits);
|
||||||
|
|
||||||
|
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[1];
|
||||||
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = buffer[0];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||||
|
{
|
||||||
|
var bytes = new byte[capacity];
|
||||||
|
for (var index = 0; index < bytes.Length; index++)
|
||||||
|
{
|
||||||
|
Span<byte> current = stackalloc byte[1];
|
||||||
|
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||||
|
{
|
||||||
|
value = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (current[0] == 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytes[index] = current[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
value = Encoding.UTF8.GetString(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
var value = (int)result;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceEventFlag(string message)
|
private static void TraceEventFlag(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
|
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)
|
private static void AppendByte(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
if (ctx.TryReadByte(address, out var value))
|
if (TryReadByte(ctx, address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X2}");
|
builder.Append($" {name}=0x{value:X2}");
|
||||||
}
|
}
|
||||||
@@ -543,7 +622,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
private static void AppendUInt32(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
if (ctx.TryReadUInt32(address, out var value))
|
if (TryReadUInt32(ctx, address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X8}");
|
builder.Append($" {name}=0x{value:X8}");
|
||||||
}
|
}
|
||||||
@@ -551,7 +630,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
private static void AppendUInt64(StringBuilder builder, CpuContext ctx, ulong address, string name)
|
||||||
{
|
{
|
||||||
if (ctx.TryReadUInt64(address, out var value))
|
if (TryReadUInt64(ctx, address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X16}");
|
builder.Append($" {name}=0x{value:X16}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -276,24 +276,8 @@ public static class KernelEventQueueCompatExports
|
|||||||
var outCountAddress = ctx[CpuRegister.Rcx];
|
var outCountAddress = ctx[CpuRegister.Rcx];
|
||||||
var timeoutAddress = ctx[CpuRegister.R8];
|
var timeoutAddress = ctx[CpuRegister.R8];
|
||||||
|
|
||||||
if (!IsValidEqueue(handle))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (eventsAddress == 0 || eventCapacity < 1)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
uint timeoutUsec = 0;
|
|
||||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -316,41 +300,6 @@ public static class KernelEventQueueCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutAddress != 0 && ctx.TryReadUInt64(timeoutAddress, out var timeoutRaw))
|
|
||||||
{
|
|
||||||
var timeoutMicros = timeoutRaw & 0xFFFF_FFFFUL;
|
|
||||||
var deadline = Environment.TickCount64 +
|
|
||||||
Math.Max(1L, (long)Math.Min(timeoutMicros / 1000, int.MaxValue));
|
|
||||||
lock (_eventQueueGate)
|
|
||||||
{
|
|
||||||
while (!HasPendingEvents(handle))
|
|
||||||
{
|
|
||||||
var remaining = deadline - Environment.TickCount64;
|
|
||||||
if (remaining <= 0)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
Monitor.Wait(_eventQueueGate, (int)Math.Min(remaining, 100));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
|
||||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (deliveredCount > 0)
|
|
||||||
{
|
|
||||||
TraceEventQueue(ctx, "wait-timed-deliver", handle);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceEventQueue(ctx, "wait-timeout", handle);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceEventQueue(ctx, "wait", handle);
|
TraceEventQueue(ctx, "wait", handle);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -381,7 +330,6 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
queue.AddLast(queuedEvent);
|
queue.AddLast(queuedEvent);
|
||||||
queued = true;
|
queued = true;
|
||||||
Monitor.PulseAll(_eventQueueGate);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (queued)
|
if (queued)
|
||||||
@@ -541,14 +489,12 @@ public static class KernelEventQueueCompatExports
|
|||||||
ulong outCountAddress)
|
ulong outCountAddress)
|
||||||
{
|
{
|
||||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
return deliveredCount > 0
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool HasPendingEvents(ulong handle)
|
private static bool HasPendingEvents(ulong handle)
|
||||||
@@ -654,8 +600,16 @@ public static class KernelEventQueueCompatExports
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
|
var returnRip = 0UL;
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||||
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,65 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
|
||||||
|
|
||||||
public static class KernelExceptionCompatExports
|
|
||||||
{
|
|
||||||
private static readonly HashSet<int> AllowedSignals = new() { 1, 4, 8, 10, 11, 30 };
|
|
||||||
private static readonly Dictionary<int, ulong> _installedHandlers = new();
|
|
||||||
private static readonly object _gate = new();
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "WkwEd3N7w0Y",
|
|
||||||
ExportName = "sceKernelInstallExceptionHandler",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int InstallExceptionHandler(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var signum = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
var handler = ctx[CpuRegister.Rsi];
|
|
||||||
|
|
||||||
if (!AllowedSignals.Contains(signum))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
if (_installedHandlers.ContainsKey(signum))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_ALREADY_EXISTS;
|
|
||||||
}
|
|
||||||
|
|
||||||
_installedHandlers[signum] = handler;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Qhv5ARAoOEc",
|
|
||||||
ExportName = "sceKernelRemoveExceptionHandler",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int RemoveExceptionHandler(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var signum = unchecked((int)ctx[CpuRegister.Rdi]);
|
|
||||||
|
|
||||||
if (!AllowedSignals.Contains(signum))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
lock (_gate)
|
|
||||||
{
|
|
||||||
_installedHandlers.Remove(signum);
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -8,13 +8,12 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
|
|
||||||
public static class KernelExports
|
public static class KernelExports
|
||||||
{
|
{
|
||||||
|
private static int _nextFileDescriptor = 2;
|
||||||
private static readonly object _cxaGate = new();
|
private static readonly object _cxaGate = new();
|
||||||
private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
|
private static readonly List<CxaDestructorEntry> _cxaDestructors = new();
|
||||||
private static readonly object _coredumpGate = new();
|
private static readonly object _coredumpGate = new();
|
||||||
private static ulong _coredumpHandler;
|
private static ulong _coredumpHandler;
|
||||||
private static ulong _coredumpHandlerContext;
|
private static ulong _coredumpHandlerContext;
|
||||||
private const uint Gen4CompiledSdkVersion = 0x05000000;
|
|
||||||
private const uint Gen5CompiledSdkVersion = 0x09000000;
|
|
||||||
|
|
||||||
private readonly record struct CxaDestructorEntry(
|
private readonly record struct CxaDestructorEntry(
|
||||||
ulong Function,
|
ulong Function,
|
||||||
@@ -28,24 +27,7 @@ public static class KernelExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var versionAddress = ctx[CpuRegister.Rdi];
|
_ = ctx;
|
||||||
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;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -71,20 +53,12 @@ public static class KernelExports
|
|||||||
ExportName = "exit",
|
ExportName = "exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
|
public static int Exit(CpuContext ctx)
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "L1SBTkC+Cvw",
|
|
||||||
ExportName = "abort",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libc")]
|
|
||||||
public static int Abort(CpuContext ctx)
|
|
||||||
{
|
{
|
||||||
// Route through the same graceful guest-entry-exit path as exit(): letting the call
|
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
// fall through to the host's native abort() does not unwind the guest thread cleanly.
|
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
|
||||||
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
|
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
|
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,7 +173,11 @@ public static class KernelExports
|
|||||||
ExportName = "_exit",
|
ExportName = "_exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
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(
|
[SysAbiExport(
|
||||||
Nid = "Ac86z8q7T8A",
|
Nid = "Ac86z8q7T8A",
|
||||||
@@ -278,14 +256,20 @@ public static class KernelExports
|
|||||||
ExportName = "pthread_create",
|
ExportName = "pthread_create",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(ctx);
|
public static int PosixPthreadCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return PthreadCreate(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Jmi+9w9u0E4",
|
Nid = "Jmi+9w9u0E4",
|
||||||
ExportName = "pthread_create_name_np",
|
ExportName = "pthread_create_name_np",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
|
public static int PosixPthreadCreateNameNp(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return PthreadCreate(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "3kg7rT0NQIs",
|
Nid = "3kg7rT0NQIs",
|
||||||
@@ -362,7 +346,10 @@ public static class KernelExports
|
|||||||
ExportName = "pthread_join",
|
ExportName = "pthread_join",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
|
public static int PosixPthreadJoin(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return PthreadJoin(ctx);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "wuCroIGjt2g",
|
Nid = "wuCroIGjt2g",
|
||||||
@@ -378,13 +365,6 @@ public static class KernelExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
public static int KernelOpen(CpuContext ctx) => KernelMemoryCompatExports.KernelOpenUnderscore(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "mqQMh1zPPT8",
|
|
||||||
ExportName = "fstat",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libc")]
|
|
||||||
public static int Fstat(CpuContext ctx) => KernelMemoryCompatExports.KernelFstat(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hcuQgD53UxM",
|
Nid = "hcuQgD53UxM",
|
||||||
ExportName = "printf",
|
ExportName = "printf",
|
||||||
@@ -451,76 +431,4 @@ public static class KernelExports
|
|||||||
{
|
{
|
||||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
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;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,8 @@
|
|||||||
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Threading;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -93,13 +95,6 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "7Xl257M4VNI",
|
|
||||||
ExportName = "pthread_equal",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libc")]
|
|
||||||
public static int PosixPthreadEqual(CpuContext ctx) => PthreadEqual(ctx);
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "T72hz6ffq08",
|
Nid = "T72hz6ffq08",
|
||||||
ExportName = "scePthreadYield",
|
ExportName = "scePthreadYield",
|
||||||
@@ -327,12 +322,6 @@ public static class KernelPthreadCompatExports
|
|||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
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(
|
[SysAbiExport(
|
||||||
Nid = "m5-2bsNfv7s",
|
Nid = "m5-2bsNfv7s",
|
||||||
@@ -387,42 +376,42 @@ public static class KernelPthreadCompatExports
|
|||||||
var initRoutine = ctx[CpuRegister.Rsi];
|
var initRoutine = ctx[CpuRegister.Rsi];
|
||||||
if (onceAddress == 0 || initRoutine == 0)
|
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)
|
if (onceValue == PthreadOnceDone)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
var gate = GetPthreadOnceGate(onceAddress);
|
var gate = GetPthreadOnceGate(onceAddress);
|
||||||
var shouldCall = false;
|
var shouldCall = false;
|
||||||
lock (gate)
|
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)
|
while (onceValue == PthreadOnceInProgress)
|
||||||
{
|
{
|
||||||
Monitor.Wait(gate, TimeSpan.FromMilliseconds(1));
|
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 (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;
|
shouldCall = true;
|
||||||
@@ -438,21 +427,21 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
lock (gate)
|
lock (gate)
|
||||||
{
|
{
|
||||||
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||||
Monitor.PulseAll(gate);
|
Monitor.PulseAll(gate);
|
||||||
}
|
}
|
||||||
|
|
||||||
TracePthreadOnce(onceAddress, initRoutine, "failed", error);
|
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)
|
lock (gate)
|
||||||
{
|
{
|
||||||
if (!ctx.TryWriteInt32(onceAddress, PthreadOnceDone))
|
if (!TryWriteInt32(ctx, onceAddress, PthreadOnceDone))
|
||||||
{
|
{
|
||||||
_ = ctx.TryWriteInt32(onceAddress, PthreadOnceUninitialized);
|
_ = TryWriteInt32(ctx, onceAddress, PthreadOnceUninitialized);
|
||||||
Monitor.PulseAll(gate);
|
Monitor.PulseAll(gate);
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor.PulseAll(gate);
|
Monitor.PulseAll(gate);
|
||||||
@@ -460,7 +449,7 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
TracePthreadOnce(onceAddress, initRoutine, shouldCall ? "call" : "done", null);
|
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)
|
private static int PthreadMutexInitCore(CpuContext ctx, ulong mutexAddress, ulong attrAddress)
|
||||||
@@ -1022,12 +1011,19 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
private static bool InitializeMutexObject(CpuContext ctx, ulong address, PthreadMutexState state) =>
|
||||||
ctx.TryWriteUInt32(address + 0x20, unchecked((uint)state.Type)) &&
|
TryWriteUInt32(ctx, address + 0x20, unchecked((uint)state.Type)) &&
|
||||||
ctx.TryWriteUInt32(address + 0x3C, unchecked((uint)state.Protocol));
|
TryWriteUInt32(ctx, address + 0x3C, unchecked((uint)state.Protocol));
|
||||||
|
|
||||||
private static bool WriteMutexAttrObject(CpuContext ctx, ulong address, PthreadMutexAttrState state) =>
|
private static bool WriteMutexAttrObject(CpuContext ctx, ulong address, PthreadMutexAttrState state) =>
|
||||||
ctx.TryWriteUInt32(address, unchecked((uint)state.Type)) &&
|
TryWriteUInt32(ctx, address, unchecked((uint)state.Type)) &&
|
||||||
ctx.TryWriteUInt32(address + 4, unchecked((uint)state.Protocol));
|
TryWriteUInt32(ctx, address + 4, unchecked((uint)state.Protocol));
|
||||||
|
|
||||||
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(uint)];
|
||||||
|
BitConverter.TryWriteBytes(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
private static int PthreadCondInitCore(CpuContext ctx, ulong condAddress)
|
private static int PthreadCondInitCore(CpuContext ctx, ulong condAddress)
|
||||||
{
|
{
|
||||||
@@ -1319,6 +1315,32 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadInt32(CpuContext ctx, ulong address, out int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
if (!ctx.Memory.TryRead(address, bytes))
|
||||||
|
{
|
||||||
|
value = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteInt32(CpuContext ctx, ulong address, int value)
|
||||||
|
{
|
||||||
|
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||||
|
return ctx.Memory.TryWrite(address, bytes);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
|
private static bool CreateImplicitMutexState(CpuContext ctx, ulong mutexAddress, int type, out ulong resolvedAddress, [NotNullWhen(true)] out PthreadMutexState? state)
|
||||||
{
|
{
|
||||||
var createdState = new PthreadMutexState
|
var createdState = new PthreadMutexState
|
||||||
|
|||||||
@@ -2,8 +2,10 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
using System.Diagnostics.CodeAnalysis;
|
using System.Diagnostics.CodeAnalysis;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -20,7 +22,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
private const int DefaultSchedPriority = DefaultThreadPriority;
|
private const int DefaultSchedPriority = DefaultThreadPriority;
|
||||||
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
|
private const ulong SyntheticRwlockHandleBase = 0x00006003_0000_0000;
|
||||||
private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_0000_0000;
|
private const ulong SyntheticPthreadAttrHandleBase = 0x00006004_0000_0000;
|
||||||
private const ulong SyntheticRwlockAttrHandleBase = 0x00006005_0000_0000;
|
|
||||||
|
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
|
private static readonly Dictionary<ulong, ThreadState> _threadStates = new();
|
||||||
@@ -30,7 +31,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
private static int _nextTlsKey = 1;
|
private static int _nextTlsKey = 1;
|
||||||
private static long _nextSyntheticRwlockHandleId = 1;
|
private static long _nextSyntheticRwlockHandleId = 1;
|
||||||
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
||||||
private static long _nextSyntheticRwlockAttrHandleId = 1;
|
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
||||||
|
|
||||||
@@ -277,7 +277,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
priority = GetOrCreateThreadStateLocked(thread).Priority;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(outPriorityAddress, priority))
|
if (!TryWriteInt32(ctx, outPriorityAddress, priority))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -324,7 +324,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -494,7 +494,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
state = GetOrCreateAttrStateLocked(attrAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(outStateAddress, state.DetachState))
|
if (!TryWriteInt32(ctx, outStateAddress, state.DetachState))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -717,35 +717,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "FXPWHNk8Of0",
|
|
||||||
ExportName = "scePthreadAttrGetschedparam",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrGetschedparam(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var attrAddress = ctx[CpuRegister.Rdi];
|
|
||||||
var schedParamAddress = ctx[CpuRegister.Rsi];
|
|
||||||
if (attrAddress == 0 || schedParamAddress == 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
PthreadAttrState state;
|
|
||||||
lock (_stateGate)
|
|
||||||
{
|
|
||||||
state = GetOrCreateAttrStateLocked(attrAddress);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(schedParamAddress, state.SchedPriority))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "DzES9hQF4f4",
|
Nid = "DzES9hQF4f4",
|
||||||
ExportName = "scePthreadAttrSetschedparam",
|
ExportName = "scePthreadAttrSetschedparam",
|
||||||
@@ -760,7 +731,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -839,37 +810,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return PthreadAttrSetstacksize(ctx);
|
return PthreadAttrSetstacksize(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "Bvn74vj6oLo",
|
|
||||||
ExportName = "scePthreadAttrSetstack",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadAttrSetstack(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var attrAddress = ctx[CpuRegister.Rdi];
|
|
||||||
var stackAddress = ctx[CpuRegister.Rsi];
|
|
||||||
var stackSize = ctx[CpuRegister.Rdx];
|
|
||||||
if (attrAddress == 0 || stackAddress == 0 || stackSize == 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var resolvedAddress = ResolvePthreadAttrHandle(ctx, attrAddress);
|
|
||||||
lock (_stateGate)
|
|
||||||
{
|
|
||||||
var state = GetOrCreateAttrStateLocked(resolvedAddress);
|
|
||||||
var updated = state with { StackAddress = stackAddress, StackSize = stackSize };
|
|
||||||
_attrStates[resolvedAddress] = updated;
|
|
||||||
if (resolvedAddress != attrAddress)
|
|
||||||
{
|
|
||||||
_attrStates[attrAddress] = updated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "6ULAa0fq4jA",
|
Nid = "6ULAa0fq4jA",
|
||||||
ExportName = "scePthreadRwlockInit",
|
ExportName = "scePthreadRwlockInit",
|
||||||
@@ -1047,47 +987,6 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadRwlockUnlock(CpuContext ctx) => PthreadRwlockUnlock(ctx);
|
public static int PosixPthreadRwlockUnlock(CpuContext ctx) => PthreadRwlockUnlock(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "yOfGg-I1ZII",
|
|
||||||
ExportName = "scePthreadRwlockattrInit",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadRwlockattrInit(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var attrAddress = ctx[CpuRegister.Rdi];
|
|
||||||
if (attrAddress == 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
var syntheticHandle = AllocateSyntheticHandle(SyntheticRwlockAttrHandleBase, ref _nextSyntheticRwlockAttrHandleId);
|
|
||||||
if (!KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, syntheticHandle))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "i2ifZ3fS2fo",
|
|
||||||
ExportName = "scePthreadRwlockattrDestroy",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PthreadRwlockattrDestroy(CpuContext ctx)
|
|
||||||
{
|
|
||||||
var attrAddress = ctx[CpuRegister.Rdi];
|
|
||||||
if (attrAddress == 0)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
_ = KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, attrAddress, 0);
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "mqULNdimTn0",
|
Nid = "mqULNdimTn0",
|
||||||
ExportName = "pthread_key_create",
|
ExportName = "pthread_key_create",
|
||||||
@@ -1112,12 +1011,12 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(outKeyAddress, key))
|
if (!TryWriteInt32(ctx, outKeyAddress, key))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = unchecked((uint)key);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1449,4 +1348,24 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
payload[^1] = 0;
|
payload[^1] = 0;
|
||||||
return ctx.Memory.TryWrite(address, payload);
|
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,20 @@
|
|||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Fiber;
|
using SharpEmu.Libs.Fiber;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
|
using System.Collections.Generic;
|
||||||
using System.Diagnostics;
|
using System.Diagnostics;
|
||||||
|
using System.IO;
|
||||||
|
using System.Reflection;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Runtime.Intrinsics.X86;
|
using System.Runtime.Intrinsics.X86;
|
||||||
using System.Security.Cryptography;
|
using System.Security.Cryptography;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Threading;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
public static class KernelRuntimeCompatExports
|
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 TlsErrnoOffset = 0x40;
|
||||||
private const ulong TlsStackChkGuardBaseOffset = 0x800;
|
private const ulong TlsStackChkGuardBaseOffset = 0x800;
|
||||||
private const ulong StackChkGuardFieldOffset = 0x10;
|
private const ulong StackChkGuardFieldOffset = 0x10;
|
||||||
@@ -80,77 +68,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
private delegate ulong RdtscDelegate();
|
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(
|
[SysAbiExport(
|
||||||
Nid = "1jfXLRVzisc",
|
Nid = "1jfXLRVzisc",
|
||||||
ExportName = "sceKernelUsleep",
|
ExportName = "sceKernelUsleep",
|
||||||
@@ -170,11 +87,11 @@ public static class KernelRuntimeCompatExports
|
|||||||
|
|
||||||
if (micros < 1000)
|
if (micros < 1000)
|
||||||
{
|
{
|
||||||
// Guest worker pools use usleep(1) as a polling backoff. Do not turn
|
// Guest worker pools use usleep(1) as a polling backoff. Periodically
|
||||||
// PS microsecond waits into Windows millisecond sleeps on hot paths.
|
// relinquish a full host time slice so spin workers cannot starve producers.
|
||||||
if ((++_shortUsleepCount & 255) == 0)
|
if ((++_shortUsleepCount & 31) == 0)
|
||||||
{
|
{
|
||||||
Thread.Sleep(0);
|
Thread.Sleep(1);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -273,16 +190,21 @@ public static class KernelRuntimeCompatExports
|
|||||||
|
|
||||||
long seconds;
|
long seconds;
|
||||||
long nanoseconds;
|
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];
|
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
|
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)nanoseconds)))
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
|
|
||||||
|
|
||||||
if (!ctx.Memory.TryWrite(timeAddress, timespecBuffer))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -307,11 +229,8 @@ public static class KernelRuntimeCompatExports
|
|||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
var seconds = now.ToUnixTimeSeconds();
|
var seconds = now.ToUnixTimeSeconds();
|
||||||
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
|
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
|
||||||
|
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
|
||||||
Span<byte> timevalBuffer = stackalloc byte[16];
|
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds)))
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer, seconds);
|
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer[sizeof(long)..], microseconds);
|
|
||||||
if (!ctx.Memory.TryWrite(timeAddress, timevalBuffer))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -333,28 +252,18 @@ public static class KernelRuntimeCompatExports
|
|||||||
var seconds = now.ToUnixTimeSeconds();
|
var seconds = now.ToUnixTimeSeconds();
|
||||||
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
|
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];
|
return -1;
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer, seconds);
|
|
||||||
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer[sizeof(long)..], microseconds);
|
|
||||||
if (!ctx.Memory.TryWrite(timeAddress, timevalBuffer))
|
|
||||||
{
|
|
||||||
TrySetErrno(ctx, Efault);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timezoneAddress != 0)
|
if (timezoneAddress != 0 &&
|
||||||
|
(!TryWriteInt32(ctx, timezoneAddress, 0) ||
|
||||||
|
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), 0)))
|
||||||
{
|
{
|
||||||
Span<byte> timezoneBuffer = stackalloc byte[8];
|
return -1;
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(timezoneBuffer, 0);
|
|
||||||
BinaryPrimitives.WriteInt32LittleEndian(timezoneBuffer[sizeof(int)..], 0);
|
|
||||||
if (!ctx.Memory.TryWrite(timezoneAddress, timezoneBuffer))
|
|
||||||
{
|
|
||||||
TrySetErrno(ctx, Efault);
|
|
||||||
return -1;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -709,58 +618,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
internal static bool TrySetErrno(CpuContext ctx, int value)
|
internal static bool TrySetErrno(CpuContext ctx, int value)
|
||||||
{
|
{
|
||||||
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
var address = GetTlsScratchAddress(ctx, TlsErrnoOffset);
|
||||||
return address != 0 && ctx.TryWriteInt32(address, value);
|
return address != 0 && TryWriteInt32(ctx, 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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -881,11 +739,8 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ShouldTraceVirtualMemory())
|
Console.Error.WriteLine(
|
||||||
{
|
$"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16} flags=0x{flags:X8} align=0x{effectiveAlignment:X16}");
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] reserve_virtual_range: req=0x{requestedAddress:X16} desired=0x{desiredAddress:X16} mapped=0x{mappedAddress:X16} len=0x{length:X16} flags=0x{flags:X8} align=0x{effectiveAlignment:X16}");
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
if (!ctx.TryWriteUInt64(inOutAddressPointer, mappedAddress))
|
||||||
{
|
{
|
||||||
@@ -897,7 +752,6 @@ public static class KernelRuntimeCompatExports
|
|||||||
_nextReservedVirtualBase = Math.Max(_nextReservedVirtualBase, mappedAddress + length);
|
_nextReservedVirtualBase = Math.Max(_nextReservedVirtualBase, mappedAddress + length);
|
||||||
}
|
}
|
||||||
|
|
||||||
KernelMemoryCompatExports.RegisterReservedVirtualRange(mappedAddress, length);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -970,7 +824,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
||||||
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1000,7 +854,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
var moduleHandle = ResolveModuleHandleByAddress(queriedAddress);
|
||||||
if (!ctx.TryWriteInt32(outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
if (!TryWriteInt32(ctx, outInfoAddress + ModuleInfoHandleOffset, moduleHandle))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1201,7 +1055,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
public static int KernelStopUnloadModule(CpuContext ctx)
|
public static int KernelStopUnloadModule(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var resultAddress = ctx[CpuRegister.R9];
|
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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1219,7 +1073,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
{
|
{
|
||||||
var modulePathAddress = ctx[CpuRegister.Rdi];
|
var modulePathAddress = ctx[CpuRegister.Rdi];
|
||||||
var resultAddress = ctx[CpuRegister.R9];
|
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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1376,8 +1230,8 @@ public static class KernelRuntimeCompatExports
|
|||||||
: 0;
|
: 0;
|
||||||
var minutesWest = unchecked((int)-offset.TotalMinutes);
|
var minutesWest = unchecked((int)-offset.TotalMinutes);
|
||||||
|
|
||||||
if (!ctx.TryWriteInt32(timezoneAddress, minutesWest) ||
|
if (!TryWriteInt32(ctx, timezoneAddress, minutesWest) ||
|
||||||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), dstSeconds / 60))
|
!TryWriteInt32(ctx, timezoneAddress + sizeof(int), dstSeconds / 60))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1387,7 +1241,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1447,7 +1301,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
_loadedSysmodules.Add(moduleId);
|
_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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1537,7 +1391,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1574,7 +1428,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
var writableCount = (int)Math.Min(Math.Min(capacity, (ulong)int.MaxValue), (ulong)handles.Length);
|
var writableCount = (int)Math.Min(Math.Min(capacity, (ulong)int.MaxValue), (ulong)handles.Length);
|
||||||
for (var i = 0; i < writableCount; i++)
|
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;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -1615,42 +1469,26 @@ public static class KernelRuntimeCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int pageSize = 4096;
|
var bytes = new List<byte>(Math.Min(maxLength, 64));
|
||||||
const int inlineChunkSize = 64;
|
Span<byte> one = stackalloc byte[1];
|
||||||
Span<byte> buffer = stackalloc byte[Math.Min(maxLength, 512)];
|
for (var i = 0; i < maxLength; i++)
|
||||||
var length = 0;
|
|
||||||
|
|
||||||
for (var offset = 0; offset < maxLength && length < buffer.Length;)
|
|
||||||
{
|
{
|
||||||
var current = address + (ulong)offset;
|
if (!ctx.Memory.TryRead(address + (ulong)i, one))
|
||||||
var pageRemaining = pageSize - (int)(current & (pageSize - 1));
|
|
||||||
var chunkSize = Math.Min(
|
|
||||||
buffer.Length - length,
|
|
||||||
Math.Min(maxLength - offset, Math.Min(inlineChunkSize, pageRemaining)));
|
|
||||||
|
|
||||||
if (chunkSize <= 0)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var chunk = buffer.Slice(length, chunkSize);
|
if (one[0] == 0)
|
||||||
if (!ctx.Memory.TryRead(current, chunk))
|
|
||||||
{
|
{
|
||||||
return false;
|
value = Encoding.UTF8.GetString(bytes.ToArray());
|
||||||
}
|
|
||||||
|
|
||||||
var nulIndex = chunk.IndexOf((byte)0);
|
|
||||||
if (nulIndex >= 0)
|
|
||||||
{
|
|
||||||
value = Encoding.UTF8.GetString(buffer[..(length + nulIndex)]);
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
length += chunkSize;
|
bytes.Add(one[0]);
|
||||||
offset += chunkSize;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return false;
|
value = Encoding.UTF8.GetString(bytes.ToArray());
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
||||||
@@ -1663,6 +1501,13 @@ public static class KernelRuntimeCompatExports
|
|||||||
return unchecked(ctx.FsBase + offset);
|
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()
|
private static nint AllocateStackChkGuardObject()
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -1859,7 +1704,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
ReadOnlySpan<byte> stub = stackalloc byte[]
|
Span<byte> stub = stackalloc byte[]
|
||||||
{
|
{
|
||||||
0x0F, 0x31,
|
0x0F, 0x31,
|
||||||
0x48, 0xC1, 0xE2, 0x20,
|
0x48, 0xC1, 0xE2, 0x20,
|
||||||
@@ -1867,14 +1712,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
0xC3,
|
0xC3,
|
||||||
};
|
};
|
||||||
|
|
||||||
unsafe
|
Marshal.Copy(stub.ToArray(), 0, stubAddress, stub.Length);
|
||||||
{
|
|
||||||
fixed (byte* src = stub)
|
|
||||||
{
|
|
||||||
Buffer.MemoryCopy(src, (void*)stubAddress, stub.Length, stub.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress);
|
return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
@@ -1894,16 +1732,117 @@ public static class KernelRuntimeCompatExports
|
|||||||
bool allowSearch,
|
bool allowSearch,
|
||||||
out ulong mappedAddress)
|
out ulong mappedAddress)
|
||||||
{
|
{
|
||||||
return KernelVirtualRangeAllocator.TryReserve(
|
mappedAddress = 0;
|
||||||
ctx,
|
if (length == 0)
|
||||||
desiredAddress,
|
{
|
||||||
length,
|
return false;
|
||||||
executable: false,
|
}
|
||||||
alignment,
|
|
||||||
allowSearch,
|
try
|
||||||
allowAllocateAtAlternative: allowSearch,
|
{
|
||||||
"reserve_virtual_range",
|
object memoryObject = ctx.Memory;
|
||||||
out mappedAddress);
|
MethodInfo? allocateAt = null;
|
||||||
|
MethodInfo? allocateAtOrAbove = null;
|
||||||
|
var allocateAtHasAllowAlternativeArg = false;
|
||||||
|
for (var depth = 0; depth < 4; depth++)
|
||||||
|
{
|
||||||
|
foreach (var candidate in memoryObject.GetType().GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
||||||
|
{
|
||||||
|
var parameters = candidate.GetParameters();
|
||||||
|
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
|
||||||
|
parameters.Length == 5 &&
|
||||||
|
parameters[0].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[1].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[2].ParameterType == typeof(bool) &&
|
||||||
|
parameters[3].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
|
||||||
|
{
|
||||||
|
allocateAtOrAbove = candidate;
|
||||||
|
}
|
||||||
|
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
if (parameters.Length == 3 &&
|
||||||
|
parameters[0].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[1].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[2].ParameterType == typeof(bool))
|
||||||
|
{
|
||||||
|
allocateAt = candidate;
|
||||||
|
allocateAtHasAllowAlternativeArg = false;
|
||||||
|
}
|
||||||
|
else if (parameters.Length == 4 &&
|
||||||
|
parameters[0].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[1].ParameterType == typeof(ulong) &&
|
||||||
|
parameters[2].ParameterType == typeof(bool) &&
|
||||||
|
parameters[3].ParameterType == typeof(bool))
|
||||||
|
{
|
||||||
|
allocateAt = candidate;
|
||||||
|
allocateAtHasAllowAlternativeArg = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allocateAtOrAbove is not null && allocateAt is not null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allocateAtOrAbove is not null || allocateAt is not null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var innerProperty = memoryObject.GetType().GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance);
|
||||||
|
if (innerProperty is null)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
var innerValue = innerProperty.GetValue(memoryObject);
|
||||||
|
if (innerValue is null || ReferenceEquals(innerValue, memoryObject))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
memoryObject = innerValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allowSearch && allocateAtOrAbove is not null)
|
||||||
|
{
|
||||||
|
var searchArgs = new object[] { desiredAddress, length, false, alignment, 0UL };
|
||||||
|
var searchResult = allocateAtOrAbove.Invoke(memoryObject, searchArgs);
|
||||||
|
if (searchResult is bool trueValue && trueValue &&
|
||||||
|
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
||||||
|
{
|
||||||
|
mappedAddress = searchedAddress;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (allocateAt is null)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var invokeArgs = allocateAtHasAllowAlternativeArg
|
||||||
|
? new object[] { desiredAddress, length, false, allowSearch }
|
||||||
|
: new object[] { desiredAddress, length, false };
|
||||||
|
var result = allocateAt.Invoke(memoryObject, invokeArgs);
|
||||||
|
if (result is not ulong allocated || allocated == 0)
|
||||||
|
{
|
||||||
|
var resultType = result?.GetType().FullName ?? "null";
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] reserve_virtual_range: AllocateAt returned {resultType} value={result ?? "null"}");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mappedAddress = allocated;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine("[LOADER][TRACE] reserve_virtual_range: AllocateAt invocation threw");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong AlignUp(ulong value, ulong alignment)
|
private static ulong AlignUp(ulong value, ulong alignment)
|
||||||
@@ -1916,9 +1855,4 @@ public static class KernelRuntimeCompatExports
|
|||||||
var mask = alignment - 1;
|
var mask = alignment - 1;
|
||||||
return (value + mask) & ~mask;
|
return (value + mask) & ~mask;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ShouldTraceVirtualMemory()
|
|
||||||
{
|
|
||||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIRTUAL_MEMORY"), "1", StringComparison.Ordinal);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
using System.Text;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
namespace SharpEmu.Libs.Kernel;
|
||||||
@@ -19,22 +21,9 @@ public static class KernelSemaphoreCompatExports
|
|||||||
public required int MaxCount { get; init; }
|
public required int MaxCount { get; init; }
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
public int WaitingThreads { get; set; }
|
public int WaitingThreads { get; set; }
|
||||||
public int CancelEpoch { get; set; }
|
|
||||||
public bool Deleted { get; set; }
|
|
||||||
public object Gate { get; } = new();
|
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(
|
[SysAbiExport(
|
||||||
Nid = "188x57JYp0g",
|
Nid = "188x57JYp0g",
|
||||||
ExportName = "sceKernelCreateSema",
|
ExportName = "sceKernelCreateSema",
|
||||||
@@ -57,12 +46,12 @@ public static class KernelSemaphoreCompatExports
|
|||||||
initialCount > maxCount ||
|
initialCount > maxCount ||
|
||||||
optionAddress != 0)
|
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));
|
var handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
@@ -71,32 +60,22 @@ public static class KernelSemaphoreCompatExports
|
|||||||
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
}
|
}
|
||||||
|
|
||||||
var state = new KernelSemaphoreState
|
_semaphores[handle] = new KernelSemaphoreState
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
InitialCount = initialCount,
|
InitialCount = initialCount,
|
||||||
MaxCount = maxCount,
|
MaxCount = maxCount,
|
||||||
Count = initialCount,
|
Count = initialCount,
|
||||||
};
|
};
|
||||||
_semaphores[handle] = state;
|
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
if (!TryWriteUInt32(ctx, semaphoreAddress, handle))
|
||||||
{
|
{
|
||||||
_semaphores.TryRemove(handle, out _);
|
_semaphores.TryRemove(handle, out _);
|
||||||
// Handles are sequential and guest-predictable, so a hostile guest can
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
// 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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceSemaphore($"create handle=0x{handle:X8} name='{name}' attr=0x{attr:X} init={initialCount} max={maxCount}");
|
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(
|
[SysAbiExport(
|
||||||
@@ -112,12 +91,12 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
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)
|
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)
|
lock (semaphore.Gate)
|
||||||
@@ -126,40 +105,30 @@ public static class KernelSemaphoreCompatExports
|
|||||||
{
|
{
|
||||||
semaphore.Count -= needCount;
|
semaphore.Count -= needCount;
|
||||||
TraceSemaphore($"wait handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
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 (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}");
|
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
|
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
|
||||||
{
|
|
||||||
NeedCount = needCount,
|
|
||||||
CancelEpochAtBlock = semaphore.CancelEpoch,
|
|
||||||
};
|
|
||||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
|
||||||
ctx,
|
|
||||||
"sceKernelWaitSema",
|
|
||||||
GetSemaphoreWakeKey(handle),
|
|
||||||
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
|
||||||
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
|
||||||
{
|
{
|
||||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
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++;
|
semaphore.WaitingThreads++;
|
||||||
TraceSemaphore($"wait-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count} waiters={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))
|
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)
|
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)
|
lock (semaphore.Gate)
|
||||||
@@ -188,12 +157,12 @@ public static class KernelSemaphoreCompatExports
|
|||||||
if (semaphore.Count < needCount)
|
if (semaphore.Count < needCount)
|
||||||
{
|
{
|
||||||
TraceSemaphore($"poll-busy handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
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;
|
semaphore.Count -= needCount;
|
||||||
TraceSemaphore($"poll handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
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))
|
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)
|
if (signalCount <= 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (semaphore.Gate)
|
lock (semaphore.Gate)
|
||||||
{
|
{
|
||||||
if (semaphore.Count > semaphore.MaxCount - signalCount)
|
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;
|
semaphore.Count += signalCount;
|
||||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
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(
|
[SysAbiExport(
|
||||||
@@ -249,32 +212,26 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
if (!_semaphores.TryGetValue(handle, out var semaphore))
|
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)
|
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)
|
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.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
||||||
semaphore.CancelEpoch++;
|
semaphore.WaitingThreads = 0;
|
||||||
// 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.
|
|
||||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
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(
|
[SysAbiExport(
|
||||||
@@ -287,85 +244,68 @@ public static class KernelSemaphoreCompatExports
|
|||||||
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
var handle = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
if (!_semaphores.TryRemove(handle, out var semaphore))
|
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}'");
|
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:
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
// 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)
|
|
||||||
{
|
{
|
||||||
lock (semaphore.Gate)
|
var value = (int)result;
|
||||||
{
|
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||||
return TryConsumeBlockedSemaWaitLocked(semaphore, waiter);
|
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)
|
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||||
{
|
return true;
|
||||||
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;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Resume handler: runs on the woken guest thread outside the scheduler gate;
|
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||||
// its return value becomes the guest's RAX for the resumed sceKernelWaitSema.
|
|
||||||
private static int CompleteBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
|
||||||
{
|
{
|
||||||
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
|
return false;
|
||||||
// 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 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)
|
private static void TraceSemaphore(string message)
|
||||||
|
|||||||
@@ -1,159 +0,0 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
|
||||||
|
|
||||||
using SharpEmu.HLE;
|
|
||||||
using System;
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Reflection;
|
|
||||||
|
|
||||||
namespace SharpEmu.Libs.Kernel;
|
|
||||||
|
|
||||||
internal static class KernelVirtualRangeAllocator
|
|
||||||
{
|
|
||||||
private static readonly ConcurrentDictionary<Type, Accessor> _accessors = new();
|
|
||||||
|
|
||||||
public static bool TryReserve(
|
|
||||||
CpuContext ctx,
|
|
||||||
ulong desiredAddress,
|
|
||||||
ulong length,
|
|
||||||
bool executable,
|
|
||||||
ulong alignment,
|
|
||||||
bool allowSearch,
|
|
||||||
bool allowAllocateAtAlternative,
|
|
||||||
string traceName,
|
|
||||||
out ulong mappedAddress)
|
|
||||||
{
|
|
||||||
mappedAddress = 0;
|
|
||||||
if (length == 0)
|
|
||||||
{
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (!TryResolveAccessor(ctx.Memory, out var target, out var accessor))
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {ctx.Memory.GetType().FullName}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (allowSearch && accessor.AllocateAtOrAbove is not null)
|
|
||||||
{
|
|
||||||
var searchArgs = new object[] { desiredAddress, length, executable, alignment, 0UL };
|
|
||||||
var searchResult = accessor.AllocateAtOrAbove.Invoke(target, searchArgs);
|
|
||||||
if (searchResult is bool trueValue && trueValue &&
|
|
||||||
searchArgs[4] is ulong searchedAddress && searchedAddress != 0)
|
|
||||||
{
|
|
||||||
mappedAddress = searchedAddress;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accessor.AllocateAt is null)
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt missing on {target.GetType().FullName}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
var invokeArgs = accessor.AllocateAtHasAllowAlternativeArg
|
|
||||||
? new object[] { desiredAddress, length, executable, allowAllocateAtAlternative }
|
|
||||||
: new object[] { desiredAddress, length, executable };
|
|
||||||
var result = accessor.AllocateAt.Invoke(target, invokeArgs);
|
|
||||||
if (result is not ulong allocated || allocated == 0)
|
|
||||||
{
|
|
||||||
var resultType = result?.GetType().FullName ?? "null";
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt returned {resultType} value={result ?? "null"}");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
mappedAddress = allocated;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
catch
|
|
||||||
{
|
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] {traceName}: AllocateAt invocation threw");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool TryResolveAccessor(object rootMemory, out object target, out Accessor accessor)
|
|
||||||
{
|
|
||||||
target = rootMemory;
|
|
||||||
accessor = default;
|
|
||||||
|
|
||||||
for (var depth = 0; depth < 4; depth++)
|
|
||||||
{
|
|
||||||
accessor = _accessors.GetOrAdd(target.GetType(), DiscoverAccessor);
|
|
||||||
if (accessor.AllocateAt is not null || accessor.AllocateAtOrAbove is not null)
|
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (accessor.InnerProperty is null)
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
var innerValue = accessor.InnerProperty.GetValue(target);
|
|
||||||
if (innerValue is null || ReferenceEquals(innerValue, target))
|
|
||||||
{
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
|
|
||||||
target = innerValue;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static Accessor DiscoverAccessor(Type type)
|
|
||||||
{
|
|
||||||
MethodInfo? allocateAt = null;
|
|
||||||
MethodInfo? allocateAtOrAbove = null;
|
|
||||||
var allocateAtHasAllowAlternativeArg = false;
|
|
||||||
|
|
||||||
foreach (var candidate in type.GetMethods(BindingFlags.Public | BindingFlags.Instance))
|
|
||||||
{
|
|
||||||
var parameters = candidate.GetParameters();
|
|
||||||
if (string.Equals(candidate.Name, "TryAllocateAtOrAbove", StringComparison.Ordinal) &&
|
|
||||||
parameters.Length == 5 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[4].ParameterType == typeof(ulong).MakeByRefType())
|
|
||||||
{
|
|
||||||
allocateAtOrAbove = candidate;
|
|
||||||
}
|
|
||||||
else if (string.Equals(candidate.Name, "AllocateAt", StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
if (parameters.Length == 3 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = false;
|
|
||||||
}
|
|
||||||
else if (parameters.Length == 4 &&
|
|
||||||
parameters[0].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[1].ParameterType == typeof(ulong) &&
|
|
||||||
parameters[2].ParameterType == typeof(bool) &&
|
|
||||||
parameters[3].ParameterType == typeof(bool))
|
|
||||||
{
|
|
||||||
allocateAt = candidate;
|
|
||||||
allocateAtHasAllowAlternativeArg = true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var innerProperty = type.GetProperty("Inner", BindingFlags.Public | BindingFlags.Instance);
|
|
||||||
return new Accessor(allocateAt, allocateAtOrAbove, allocateAtHasAllowAlternativeArg, innerProperty);
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct Accessor(
|
|
||||||
MethodInfo? AllocateAt,
|
|
||||||
MethodInfo? AllocateAtOrAbove,
|
|
||||||
bool AllocateAtHasAllowAlternativeArg,
|
|
||||||
PropertyInfo? InnerProperty);
|
|
||||||
}
|
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user