Compare commits
99 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| fbf2c2d00a | |||
| 8c1507777c | |||
| 8ebc43e758 | |||
| 5aadb7495a | |||
| cdee77521e | |||
| d2fca37716 | |||
| 5bee81df70 | |||
| d4bb282c49 | |||
| e1cf5b13ef | |||
| de4fc1e1a8 | |||
| 5e76554514 | |||
| 3a24db567f | |||
| a9b06974be | |||
| 19added142 | |||
| 8a40251a1c | |||
| edb4eb86a2 | |||
| 79a7437cd8 | |||
| f43f7cde9c | |||
| ef74680167 | |||
| 65a40773fa | |||
| 9ddc09ea91 | |||
| c4326a1143 | |||
| 347e33f3c9 | |||
| 48a694e509 | |||
| 165927882b | |||
| d9d1aeaef9 | |||
| 57e737b5d7 | |||
| a78b02f22b | |||
| 70ec2928ea | |||
| c618c116ba | |||
| 29021b5a71 | |||
| c0fd6a80e8 | |||
| 7337683c16 | |||
| b36ecc121c | |||
| 42f0fa0e83 | |||
| 41d61bde41 | |||
| ac96fe5292 | |||
| 9b9ca8f707 | |||
| db20387502 | |||
| 44c43c8ebd | |||
| 4b5b27e686 | |||
| 5c823d0e8b | |||
| 1679897089 | |||
| ce8f75411a | |||
| f75da92fa5 | |||
| 9eaaac7cee | |||
| d446779d34 | |||
| 585d11b93b | |||
| 17f9535f0e | |||
| 8bfbb9c7fe | |||
| 2b8fe71a82 | |||
| 29b7f086c9 | |||
| eb3d8d6eb1 | |||
| 787c75b9b3 | |||
| 2a784aa405 | |||
| 2649857cb5 | |||
| c4b4aed2c5 | |||
| 757f6ea4b5 | |||
| df3ee02f8d | |||
| dfe61ec643 | |||
| 52e17b5056 | |||
| c1f3abfd5d | |||
| b5ee2c2cb7 | |||
| 16c1b74636 | |||
| 0d89b2488b | |||
| c3019b78d6 | |||
| dd5e524879 | |||
| df9d0e6aaf | |||
| 253d0fae3f | |||
| 78d719ef9e | |||
| be3806cdf6 | |||
| dc47deee93 | |||
| 1c838cf8d6 | |||
| 2f269566a5 | |||
| 9d7afef6f6 | |||
| daeff21516 | |||
| 82619deb34 | |||
| 697ad7be80 | |||
| 73c987e49f | |||
| cd79275117 | |||
| b14ecae504 | |||
| 873f473b65 | |||
| 9a1a3789ef | |||
| 0f3d4032cb | |||
| 0c4a757695 | |||
| 5fcc8eaa37 | |||
| 21105e0346 | |||
| b424df5a64 | |||
| 7f85971e39 | |||
| e28259a99d | |||
| 3d2a4b74ac | |||
| af35389bfd | |||
| 4ae778ef39 | |||
| a04e70c0b3 | |||
| 02222c7919 | |||
| 46b3a207c1 | |||
| 418d46beb5 | |||
| 0e922a73ee | |||
| a5172fd2c0 |
@@ -14,7 +14,7 @@ indent_size = 4
|
|||||||
indent_style = space
|
indent_style = space
|
||||||
tab_width = 4
|
tab_width = 4
|
||||||
|
|
||||||
[*.{md,json,yml,props,csproj}]
|
[*.{md,json,yml,xml,props,csproj}]
|
||||||
|
|
||||||
indent_size = 2
|
indent_size = 2
|
||||||
tab_width = 2
|
tab_width = 2
|
||||||
|
|||||||
@@ -16,6 +16,11 @@ 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:
|
||||||
@@ -73,11 +78,20 @@ body:
|
|||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Log File
|
label: Log File
|
||||||
description: Drag and drop your log file here, or click to browse
|
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.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: true
|
||||||
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:
|
||||||
@@ -119,7 +133,8 @@ body:
|
|||||||
id: emulator_version
|
id: emulator_version
|
||||||
attributes:
|
attributes:
|
||||||
label: Emulator Version / Commit
|
label: Emulator Version / Commit
|
||||||
placeholder: e.g. v0.0.1 / a1b2c3d
|
description: Copy the **exact short SHA** from the top of the log. Do not enter just `0.0.1` without a commit hash.
|
||||||
|
placeholder: e.g. a1b2c3d
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
|
After Width: | Height: | Size: 933 KiB |
|
After Width: | Height: | Size: 225 KiB |
|
After Width: | Height: | Size: 120 KiB |
|
Before Width: | Height: | Size: 89 KiB |
@@ -9,11 +9,17 @@ on:
|
|||||||
- "**"
|
- "**"
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**/*.md"
|
- "**/*.md"
|
||||||
|
- "**/*.png"
|
||||||
|
- "**/*.jpeg"
|
||||||
|
- "**/*.jpg"
|
||||||
pull_request:
|
pull_request:
|
||||||
branches:
|
branches:
|
||||||
- main
|
- main
|
||||||
paths-ignore:
|
paths-ignore:
|
||||||
- "**/*.md"
|
- "**/*.md"
|
||||||
|
- "**/*.png"
|
||||||
|
- "**/*.jpeg"
|
||||||
|
- "**/*.jpg"
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
@@ -120,7 +126,7 @@ jobs:
|
|||||||
needs:
|
needs:
|
||||||
- init
|
- init
|
||||||
- build
|
- build
|
||||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
permissions:
|
||||||
contents: write
|
contents: write
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ arm64/
|
|||||||
*.userosscache
|
*.userosscache
|
||||||
*.sln.docstates
|
*.sln.docstates
|
||||||
|
|
||||||
|
.packages
|
||||||
packages/
|
packages/
|
||||||
*.nupkg
|
*.nupkg
|
||||||
.nuget/
|
.nuget/
|
||||||
@@ -36,3 +37,6 @@ packages/
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
Desktop.ini
|
Desktop.ini
|
||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
|
|
||||||
|
.vs/
|
||||||
|
.idea/
|
||||||
|
|||||||
@@ -32,4 +32,18 @@ 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,16 +1,22 @@
|
|||||||
<!--
|
<!--
|
||||||
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>
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
@@ -3,13 +3,21 @@ 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,6 +7,7 @@ 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" />
|
||||||
|
|||||||
|
After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,18 @@
|
|||||||
|
<?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>
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
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;
|
||||||
@@ -24,12 +25,45 @@ 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;
|
||||||
@@ -43,9 +77,11 @@ 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}");
|
||||||
@@ -101,6 +137,100 @@ 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;
|
||||||
@@ -670,4 +800,32 @@ 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,11 +7,14 @@ 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>
|
||||||
<OutputType>Exe</OutputType>
|
<!-- GUI subsystem so starting without arguments opens the frontend with no
|
||||||
|
console window; CLI mode re-attaches to the parent terminal's console. -->
|
||||||
|
<OutputType>WinExe</OutputType>
|
||||||
<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>
|
||||||
@@ -30,6 +33,15 @@ 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>
|
||||||
@@ -38,4 +50,17 @@ 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,6 +8,105 @@
|
|||||||
"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",
|
||||||
@@ -59,6 +158,38 @@
|
|||||||
"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",
|
||||||
@@ -73,6 +204,17 @@
|
|||||||
"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"
|
||||||
},
|
},
|
||||||
@@ -81,6 +223,7 @@
|
|||||||
"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, )"
|
||||||
}
|
}
|
||||||
@@ -88,6 +231,48 @@
|
|||||||
"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, )",
|
||||||
@@ -103,6 +288,16 @@
|
|||||||
"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, )",
|
||||||
@@ -122,9 +317,61 @@
|
|||||||
"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",
|
||||||
@@ -132,6 +379,52 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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",
|
||||||
@@ -139,6 +432,52 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"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,11 +7,14 @@ 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,
|
||||||
@@ -80,8 +83,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string processImageName = "eboot.bin",
|
string processImageName = "eboot.bin",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
Log.Debug("=== DispatchEntry START ===");
|
||||||
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -89,8 +92,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
|
||||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -103,8 +105,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
string moduleName = "module",
|
string moduleName = "module",
|
||||||
CpuExecutionOptions executionOptions = default)
|
CpuExecutionOptions executionOptions = default)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
Log.Debug("=== DispatchModuleInitializer START ===");
|
||||||
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -119,8 +121,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
|
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
|
||||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -134,7 +135,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
CpuExecutionOptions executionOptions = default,
|
CpuExecutionOptions executionOptions = default,
|
||||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
Log.Debug("DispatchEntryCore STARTING...");
|
||||||
|
|
||||||
LastEntryPoint = entryPoint;
|
LastEntryPoint = entryPoint;
|
||||||
LastTrapInfo = null;
|
LastTrapInfo = null;
|
||||||
@@ -306,7 +307,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
|||||||
LastMilestoneLog,
|
LastMilestoneLog,
|
||||||
Environment.NewLine,
|
Environment.NewLine,
|
||||||
$"CpuEngine native-only failed: {backendError}");
|
$"CpuEngine native-only failed: {backendError}");
|
||||||
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
Log.Error($"Native backend FAILED: {backendError}");
|
||||||
return FailEarly(
|
return FailEarly(
|
||||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||||
CpuExitReason.NativeBackendUnavailable);
|
CpuExitReason.NativeBackendUnavailable);
|
||||||
@@ -415,8 +416,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 (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
|
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
|
||||||
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
|
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
||||||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
||||||
@@ -456,13 +457,6 @@ 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,
|
||||||
|
|||||||
@@ -2,20 +2,35 @@
|
|||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System;
|
using System;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
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;
|
||||||
|
|
||||||
public sealed partial class DirectExecutionBackend
|
public sealed partial class DirectExecutionBackend
|
||||||
{
|
{
|
||||||
|
private static readonly ConcurrentDictionary<ulong, byte> _knownExecutablePages = new();
|
||||||
|
|
||||||
private void RecordRecentImportTrace(string traceLine)
|
private void RecordRecentImportTrace(
|
||||||
|
long dispatchIndex,
|
||||||
|
string nid,
|
||||||
|
ulong returnRip,
|
||||||
|
ulong arg0,
|
||||||
|
ulong arg1,
|
||||||
|
ulong arg2)
|
||||||
{
|
{
|
||||||
_recentImportTrace[_recentImportTraceWriteIndex] = traceLine;
|
_recentImportTrace[_recentImportTraceWriteIndex] = new RecentImportTraceEntry(
|
||||||
|
dispatchIndex,
|
||||||
|
nid,
|
||||||
|
returnRip,
|
||||||
|
arg0,
|
||||||
|
arg1,
|
||||||
|
arg2);
|
||||||
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
|
_recentImportTraceWriteIndex = (_recentImportTraceWriteIndex + 1) % _recentImportTrace.Length;
|
||||||
if (_recentImportTraceCount < _recentImportTrace.Length)
|
if (_recentImportTraceCount < _recentImportTrace.Length)
|
||||||
{
|
{
|
||||||
@@ -29,15 +44,17 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Recent import calls ({_recentImportTraceCount}):");
|
Log.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++)
|
||||||
{
|
{
|
||||||
int num2 = (num + i) % _recentImportTrace.Length;
|
int num2 = (num + i) % _recentImportTrace.Length;
|
||||||
string text = _recentImportTrace[num2];
|
var entry = _recentImportTrace[num2];
|
||||||
if (!string.IsNullOrEmpty(text))
|
if (!string.IsNullOrEmpty(entry.Nid))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][INFO] " + text);
|
Log.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}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -75,12 +92,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
list.Add(num7);
|
list.Add(num7);
|
||||||
if (num2 < 32)
|
if (num2 < 32)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
|
Log.Info($"Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
|
||||||
num2++;
|
num2++;
|
||||||
}
|
}
|
||||||
if (num >= 16384)
|
if (num >= 16384)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
|
Log.Warning($"Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -90,7 +107,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
if (num != 0)
|
if (num != 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer hits: {num}");
|
Log.Warning($"Suspicious unresolved pointer hits: {num}");
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@@ -105,18 +122,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))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
|
Log.Debug($"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("-", " ");
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
|
Log.Debug($"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))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
|
Log.Debug($"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)
|
||||||
@@ -125,7 +142,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))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
|
Log.Debug($"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++)
|
||||||
@@ -137,7 +154,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))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
|
Log.Debug($"Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -152,7 +169,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));
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
Log.Debug($"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)
|
||||||
@@ -163,21 +180,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))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
$"Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
$"Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cpuContext.Memory.TryRead(target, targetBytes))
|
if (cpuContext.Memory.TryRead(target, targetBytes))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
$"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)
|
||||||
{
|
{
|
||||||
@@ -185,8 +202,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))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
$"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)
|
||||||
@@ -197,13 +214,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))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -302,11 +319,24 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
private unsafe static bool IsExecutableAddress(ulong address)
|
private unsafe static bool IsExecutableAddress(ulong address)
|
||||||
{
|
{
|
||||||
|
var pageAddress = address & ~0xFFFUL;
|
||||||
|
if (_knownExecutablePages.ContainsKey(pageAddress))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
if (VirtualQuery((void*)address, out var lpBuffer, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
return lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
|
||||||
|
var executable = lpBuffer.State == 4096 && IsExecutableProtection(lpBuffer.Protect);
|
||||||
|
if (executable)
|
||||||
|
{
|
||||||
|
_knownExecutablePages.TryAdd(pageAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
return executable;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong AlignUp(ulong value, ulong alignment)
|
private static ulong AlignUp(ulong value, ulong alignment)
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ namespace SharpEmu.Core.Cpu.Native;
|
|||||||
|
|
||||||
public sealed partial class DirectExecutionBackend
|
public sealed partial class DirectExecutionBackend
|
||||||
{
|
{
|
||||||
|
private const ulong LazyCommitWindowBytes = 0x0200_0000UL;
|
||||||
|
private static int _lazyCommitTraceCount;
|
||||||
|
|
||||||
private unsafe void SetupExceptionHandler()
|
private unsafe void SetupExceptionHandler()
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_RAW_HANDLER"), "1", StringComparison.Ordinal))
|
||||||
@@ -100,6 +103,15 @@ 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;
|
||||||
@@ -911,7 +923,21 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
ulong pageBase = faultAddress & 0xFFFFFFFFFFFFF000uL;
|
||||||
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
uint commitProtect = ResolveLazyCommitProtection(accessType, mbi.AllocationProtect);
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
int traceIndex = Interlocked.Increment(ref _lazyCommitTraceCount);
|
||||||
|
bool traceLazyCommit = ShouldTraceLazyCommit(traceIndex);
|
||||||
|
if (traceLazyCommit)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-query#{traceIndex}: fault=0x{faultAddress:X16} owner={owner} rip=0x{rip:X16} rsp=0x{rsp:X16} state=0x{mbi.State:X08} base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} alloc=0x{mbi.AllocationProtect:X08} prot=0x{mbi.Protect:X08}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (mbi.State == 4096 && IsAccessCompatible(accessType, mbi.Protect))
|
||||||
|
{
|
||||||
|
if (traceLazyCommit)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit-race#{traceIndex}: fault=0x{faultAddress:X16} protect=0x{mbi.Protect:X08}");
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
bool committed = false;
|
bool committed = false;
|
||||||
ulong committedBase = 0;
|
ulong committedBase = 0;
|
||||||
@@ -919,14 +945,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
if (mbi.State == 65536)
|
if (mbi.State == 65536)
|
||||||
{
|
{
|
||||||
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var windowBase, out var windowSize) &&
|
||||||
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
TryReserveThenCommit(windowBase, windowSize, windowBase, windowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeBase;
|
committedBase = windowBase;
|
||||||
committedSize = 2097152uL;
|
committedSize = windowSize;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
ulong largeBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
|
if (TryReserveThenCommit(largeBase, 2097152uL, largeBase, 2097152uL, commitProtect))
|
||||||
|
{
|
||||||
|
committed = true;
|
||||||
|
committedBase = largeBase;
|
||||||
|
committedSize = 2097152uL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
if (TryReserveThenCommit(region64kBase, 65536uL, region64kBase, 65536uL, commitProtect))
|
||||||
@@ -949,7 +986,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
if (traceLazyCommit)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-reserve-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -958,14 +998,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
if (TryGetLazyCommitWindow(faultAddress, mbi.BaseAddress, mbi.RegionSize, out var commitWindowBase, out var commitWindowSize) &&
|
||||||
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
TryCommitRange(commitWindowBase, commitWindowSize, commitProtect))
|
||||||
{
|
{
|
||||||
committed = true;
|
committed = true;
|
||||||
committedBase = largeCommitBase;
|
committedBase = commitWindowBase;
|
||||||
committedSize = 2097152uL;
|
committedSize = commitWindowSize;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
|
{
|
||||||
|
ulong largeCommitBase = faultAddress & 0xFFFFFFFFFFE00000uL;
|
||||||
|
if (TryCommitRange(largeCommitBase, 2097152uL, commitProtect))
|
||||||
|
{
|
||||||
|
committed = true;
|
||||||
|
committedBase = largeCommitBase;
|
||||||
|
committedSize = 2097152uL;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!committed)
|
||||||
{
|
{
|
||||||
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
ulong region64kBase = faultAddress & 0xFFFFFFFFFFFF0000uL;
|
||||||
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
if (TryCommitRange(region64kBase, 65536uL, commitProtect))
|
||||||
@@ -994,9 +1045,46 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
|
|
||||||
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
TryCommitRange(pageBase + 4096, 4096uL, commitProtect);
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
if (traceLazyCommit)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] lazy-commit#{traceIndex}: addr=0x{committedBase:X16} size=0x{committedSize:X16} access={accessType} protect=0x{commitProtect:X8}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
|
|
||||||
|
static bool TryGetLazyCommitWindow(ulong fault, ulong regionBase, ulong regionSize, out ulong baseAddress, out ulong length)
|
||||||
|
{
|
||||||
|
baseAddress = 0;
|
||||||
|
length = 0;
|
||||||
|
if (regionSize == 0 || ulong.MaxValue - regionBase < regionSize)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong regionEnd = regionBase + regionSize;
|
||||||
|
ulong windowBase = fault & ~(LazyCommitWindowBytes - 1);
|
||||||
|
if (windowBase < regionBase)
|
||||||
|
{
|
||||||
|
windowBase = regionBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (windowBase >= regionEnd)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong windowEnd = Math.Min(regionEnd, windowBase + LazyCommitWindowBytes);
|
||||||
|
ulong windowSize = windowEnd - windowBase;
|
||||||
|
windowSize &= 0xFFFFFFFFFFFFF000uL;
|
||||||
|
if (windowSize == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
baseAddress = windowBase;
|
||||||
|
length = windowSize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
static unsafe bool TryCommitRange(ulong baseAddress, ulong length, uint protection)
|
||||||
{
|
{
|
||||||
if (length == 0)
|
if (length == 0)
|
||||||
@@ -1023,6 +1111,49 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
return TryCommitRange(commitAddress, commitSize, protection);
|
return TryCommitRange(commitAddress, commitSize, protection);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
static bool IsAccessCompatible(ulong accessType, uint protection)
|
||||||
|
{
|
||||||
|
const uint pageNoAccess = 0x01;
|
||||||
|
const uint pageReadOnly = 0x02;
|
||||||
|
const uint pageReadWrite = 0x04;
|
||||||
|
const uint pageWriteCopy = 0x08;
|
||||||
|
const uint pageExecute = 0x10;
|
||||||
|
const uint pageExecuteRead = 0x20;
|
||||||
|
const uint pageExecuteReadWrite = 0x40;
|
||||||
|
const uint pageExecuteWriteCopy = 0x80;
|
||||||
|
const uint pageGuard = 0x100;
|
||||||
|
const uint accessMask = 0xFF;
|
||||||
|
|
||||||
|
if ((protection & pageGuard) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint access = protection & accessMask;
|
||||||
|
if (access == pageNoAccess)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return accessType switch
|
||||||
|
{
|
||||||
|
0 => access is pageReadOnly or pageReadWrite or pageWriteCopy or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
|
||||||
|
1 => access is pageReadWrite or pageWriteCopy or pageExecuteReadWrite or pageExecuteWriteCopy,
|
||||||
|
8 => access is pageExecute or pageExecuteRead or pageExecuteReadWrite or pageExecuteWriteCopy,
|
||||||
|
_ => false
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldTraceLazyCommit(int traceIndex)
|
||||||
|
{
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_LAZY_COMMIT"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return traceIndex <= 16 || traceIndex % 256 == 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
|
private static uint ResolveLazyCommitProtection(ulong accessType, uint allocationProtect)
|
||||||
|
|||||||
@@ -14,8 +14,21 @@ namespace SharpEmu.Core.Cpu.Native;
|
|||||||
|
|
||||||
public sealed partial class DirectExecutionBackend
|
public sealed partial class DirectExecutionBackend
|
||||||
{
|
{
|
||||||
|
private readonly object _importResultLogSampleGate = new();
|
||||||
|
private readonly Dictionary<string, int> _importResultLogSamples = new(StringComparer.Ordinal);
|
||||||
|
|
||||||
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))
|
||||||
@@ -33,6 +46,13 @@ 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)
|
||||||
@@ -66,6 +86,12 @@ 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;
|
||||||
@@ -73,8 +99,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
private unsafe ulong DispatchImport(int importIndex, nint argPackPtr)
|
private unsafe ulong DispatchImport(int importIndex, nint argPackPtr)
|
||||||
{
|
{
|
||||||
long num = Interlocked.Increment(ref _importDispatchCount);
|
long num = NextImportDispatchIndex();
|
||||||
MarkExecutionProgress();
|
if ((num & 0x3F) == 0)
|
||||||
|
{
|
||||||
|
MarkExecutionProgress();
|
||||||
|
}
|
||||||
var cpuContext = ActiveCpuContext;
|
var cpuContext = ActiveCpuContext;
|
||||||
if (cpuContext == null)
|
if (cpuContext == null)
|
||||||
{
|
{
|
||||||
@@ -93,6 +122,18 @@ 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) &&
|
||||||
|
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
|
||||||
|
{
|
||||||
|
return leafResult;
|
||||||
|
}
|
||||||
|
|
||||||
cpuContext.Rip = importStubEntry.Address;
|
cpuContext.Rip = importStubEntry.Address;
|
||||||
cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
|
cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
|
||||||
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
|
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
|
||||||
@@ -106,6 +147,17 @@ 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
|
||||||
|
// 0x80 bytes immediately below the GP argpack, so variadic float args (printf
|
||||||
|
// %f, and powf/logf inputs) reach the handler. xmm{i} is at argPackPtr-0x80+i*16.
|
||||||
|
for (var xmmIndex = 0; xmmIndex < 8; xmmIndex++)
|
||||||
|
{
|
||||||
|
var xmmSlot = argPackPtr - 0x80 + (xmmIndex * 16);
|
||||||
|
cpuContext.SetXmmRegister(
|
||||||
|
xmmIndex,
|
||||||
|
*(ulong*)xmmSlot,
|
||||||
|
*(ulong*)(xmmSlot + 8));
|
||||||
|
}
|
||||||
cpuContext[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];
|
||||||
@@ -120,6 +172,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong value7 = cpuContext[CpuRegister.R14];
|
ulong value7 = cpuContext[CpuRegister.R14];
|
||||||
ulong value8 = cpuContext[CpuRegister.R15];
|
ulong value8 = cpuContext[CpuRegister.R15];
|
||||||
ulong num7 = *(ulong*)(argPackPtr + 96);
|
ulong num7 = *(ulong*)(argPackPtr + 96);
|
||||||
|
var isGuestWorker = GuestThreadExecution.IsGuestThread;
|
||||||
if (!IsLikelyReturnAddress(num7))
|
if (!IsLikelyReturnAddress(num7))
|
||||||
{
|
{
|
||||||
for (int i = 1; i <= 4; i++)
|
for (int i = 1; i <= 4; i++)
|
||||||
@@ -134,19 +187,29 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
TrackDistinctImportNid(importStubEntry.Nid);
|
if (_activeGuestThreadState is { } activeGuestThreadState)
|
||||||
var probeImportReturn = Environment.GetEnvironmentVariable("SHARPEMU_PROBE_IMPORT_RET");
|
{
|
||||||
if (!string.IsNullOrWhiteSpace(probeImportReturn) &&
|
Interlocked.Increment(ref activeGuestThreadState.ImportCount);
|
||||||
(string.Equals(probeImportReturn, "*", StringComparison.Ordinal) ||
|
Volatile.Write(ref activeGuestThreadState.LastImportNid, importStubEntry.Nid);
|
||||||
string.Equals(probeImportReturn, importStubEntry.Nid, StringComparison.Ordinal)))
|
Volatile.Write(ref activeGuestThreadState.LastReturnRip, num7);
|
||||||
|
}
|
||||||
|
if (_logStrlenBursts)
|
||||||
|
{
|
||||||
|
TrackDistinctImportNid(importStubEntry.Nid);
|
||||||
|
TrackStrlenPrelude(importStubEntry.Nid, num, num7);
|
||||||
|
}
|
||||||
|
if (!string.IsNullOrWhiteSpace(_probeImportReturn) &&
|
||||||
|
(string.Equals(_probeImportReturn, "*", StringComparison.Ordinal) ||
|
||||||
|
string.Equals(_probeImportReturn, importStubEntry.Nid, StringComparison.Ordinal)))
|
||||||
{
|
{
|
||||||
ProbeReturnRip(num7, num);
|
ProbeReturnRip(num7, num);
|
||||||
}
|
}
|
||||||
TrackStrlenPrelude(importStubEntry.Nid, num, num7);
|
if (_logGuestContext)
|
||||||
TraceGuestContext(
|
{
|
||||||
$"import dispatch={num} nid={importStubEntry.Nid} ret=0x{num7:X16} managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16} active={HasActiveExecutionThread}");
|
TraceGuestContext(
|
||||||
bool logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal);
|
$"import dispatch={num} nid={importStubEntry.Nid} ret=0x{num7:X16} managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16} active={HasActiveExecutionThread}");
|
||||||
if (logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
}
|
||||||
|
if (_logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
string symbolText = "<unreadable>";
|
string symbolText = "<unreadable>";
|
||||||
if (TryReadAsciiZ(value2, 256, out var sym))
|
if (TryReadAsciiZ(value2, 256, out var sym))
|
||||||
@@ -156,36 +219,56 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
|
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
|
||||||
}
|
}
|
||||||
if (!ActiveForcedGuestExit && ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) && TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
|
if (!isGuestWorker &&
|
||||||
|
!ActiveForcedGuestExit &&
|
||||||
|
ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) &&
|
||||||
|
TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
|
||||||
{
|
{
|
||||||
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;
|
||||||
bool flag3 = num >= 1020 && num <= 1040;
|
bool flag3 = num >= 1020 && num <= 1040;
|
||||||
bool logAllImports = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_ALL_IMPORTS"), "1", StringComparison.Ordinal);
|
bool flag4 = !string.IsNullOrWhiteSpace(_importFilter);
|
||||||
string importFilter = Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FILTER");
|
|
||||||
bool flag4 = !string.IsNullOrWhiteSpace(importFilter);
|
|
||||||
bool flag5 = false;
|
bool flag5 = false;
|
||||||
ExportedFunction matchedExport = null;
|
ExportedFunction? matchedExport = importStubEntry.Export;
|
||||||
if (_moduleManager.TryGetExport(importStubEntry.Nid, out ExportedFunction export))
|
bool periodicTrace = num <= 128 ||
|
||||||
|
(num >= 240 && num <= 400) ||
|
||||||
|
(num >= 900 && num <= 1300) ||
|
||||||
|
num % 100000 == 0L ||
|
||||||
|
(importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) ||
|
||||||
|
(importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) ||
|
||||||
|
flag ||
|
||||||
|
flag2 ||
|
||||||
|
flag3;
|
||||||
|
if (matchedExport is not null)
|
||||||
{
|
{
|
||||||
matchedExport = export;
|
|
||||||
if (flag4)
|
if (flag4)
|
||||||
{
|
{
|
||||||
flag5 = export.LibraryName.Contains(importFilter, StringComparison.OrdinalIgnoreCase)
|
flag5 = matchedExport.LibraryName.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase)
|
||||||
|| export.Name.Contains(importFilter, StringComparison.OrdinalIgnoreCase)
|
|| matchedExport.Name.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase)
|
||||||
|| importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase);
|
|| importStubEntry.Nid.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else if (flag4)
|
else if (flag4)
|
||||||
{
|
{
|
||||||
flag5 = importStubEntry.Nid.Contains(importFilter, StringComparison.OrdinalIgnoreCase);
|
flag5 = importStubEntry.Nid.Contains(_importFilter!, StringComparison.OrdinalIgnoreCase);
|
||||||
}
|
}
|
||||||
bool flag6 = logAllImports || flag5;
|
bool flag6 = _logAllImports || flag5;
|
||||||
if (!flag0 && (flag6 || num <= 128 || (num >= 240 && num <= 400) || (num >= 900 && num <= 1300) || num % 100000 == 0L || (importStubEntry.Nid == "tsvEmnenz48" && (num <= 256 || num % 1000 == 0L)) || (importStubEntry.Nid == "rTXw65xmLIA" && (num <= 256 || num % 128 == 0)) || flag || flag2 || flag3))
|
if (!flag0 && (flag6 || periodicTrace))
|
||||||
{
|
{
|
||||||
if (matchedExport != null)
|
if (matchedExport != null)
|
||||||
{
|
{
|
||||||
@@ -218,9 +301,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.Flush();
|
Console.Error.Flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (!flag0)
|
if (!flag0 && !isGuestWorker)
|
||||||
{
|
{
|
||||||
RecordRecentImportTrace($"#{num} nid={importStubEntry.Nid} ret=0x{num7:X16} rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} rdx=0x{cpuContext[CpuRegister.Rdx]:X16}");
|
RecordRecentImportTrace(
|
||||||
|
num,
|
||||||
|
importStubEntry.Nid,
|
||||||
|
num7,
|
||||||
|
cpuContext[CpuRegister.Rdi],
|
||||||
|
cpuContext[CpuRegister.Rsi],
|
||||||
|
cpuContext[CpuRegister.Rdx]);
|
||||||
}
|
}
|
||||||
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
|
if (importStubEntry.Nid == "8zTFvBIAIN8" && num <= 256)
|
||||||
{
|
{
|
||||||
@@ -247,11 +336,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.WriteLine($"[LOADER][TRACE] ImportStack#{num}: rsp=0x{num9:X16} [0]=0x{value9:X16} [8]=0x{value10:X16} [10]=0x{value11:X16} [18]=0x{value12:X16} [20]=0x{value13:X16} [28]=0x{value14:X16} [30]=0x{value15:X16} [38]=0x{value16:X16} [40]=0x{value17:X16}");
|
Console.Error.WriteLine($"[LOADER][TRACE] ImportStack#{num}: rsp=0x{num9:X16} [0]=0x{value9:X16} [8]=0x{value10:X16} [10]=0x{value11:X16} [18]=0x{value12:X16} [20]=0x{value13:X16} [28]=0x{value14:X16} [30]=0x{value15:X16} [38]=0x{value16:X16} [40]=0x{value17:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (flag6 && string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_FRAMES"), "1", StringComparison.Ordinal))
|
if (flag6 && _logImportFrames)
|
||||||
{
|
{
|
||||||
TraceImportFrameChain(cpuContext, num);
|
TraceImportFrameChain(cpuContext, num);
|
||||||
}
|
}
|
||||||
if (flag6 && string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_IMPORT_RECENT"), "1", StringComparison.Ordinal))
|
if (flag6 && _logImportRecent)
|
||||||
{
|
{
|
||||||
DumpRecentImportTrace();
|
DumpRecentImportTrace();
|
||||||
}
|
}
|
||||||
@@ -262,7 +351,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
if (importStubEntry.Nid == "Ou3iL1abvng")
|
if (importStubEntry.Nid == "Ou3iL1abvng")
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STACK_CHK"), "1", StringComparison.Ordinal))
|
if (_logStackCheck)
|
||||||
{
|
{
|
||||||
var savedGuardAddress = value4 >= 0x10 ? value4 - 0x10 : 0;
|
var savedGuardAddress = value4 >= 0x10 ? value4 - 0x10 : 0;
|
||||||
var guardKnown = TryReadUInt64Compat(value3, out var guardValue);
|
var guardKnown = TryReadUInt64Compat(value3, out var guardValue);
|
||||||
@@ -296,14 +385,41 @@ 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
|
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
dispatchResolved = _moduleManager.TryDispatch(importStubEntry.Nid, cpuContext, out orbisGen2Result);
|
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
|
||||||
}
|
}
|
||||||
|
else if (importStubEntry.Export is { } cachedExport &&
|
||||||
|
(cachedExport.Target & cpuContext.TargetGeneration) != 0)
|
||||||
|
{
|
||||||
|
cpuContext.ClearRaxWriteFlag();
|
||||||
|
var returnValue = cachedExport.Function(cpuContext);
|
||||||
|
if (!cpuContext.WasRaxWritten)
|
||||||
|
{
|
||||||
|
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
|
||||||
|
}
|
||||||
|
orbisGen2Result = (OrbisGen2Result)returnValue;
|
||||||
|
}
|
||||||
|
else if (importStubEntry.Export is { } mismatchedExport)
|
||||||
|
{
|
||||||
|
dispatchResolved = true;
|
||||||
|
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||||
|
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Import#{num} not implemented for generation {cpuContext.TargetGeneration}: " +
|
||||||
|
$"nid={importStubEntry.Nid} targets={mismatchedExport.Target} ret=0x{num7:X16}");
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
dispatchResolved = false;
|
||||||
|
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -318,7 +434,9 @@ public sealed partial class DirectExecutionBackend
|
|||||||
if (!dispatchResolved)
|
if (!dispatchResolved)
|
||||||
{
|
{
|
||||||
LastError = "Missing HLE export for NID: " + importStubEntry.Nid;
|
LastError = "Missing HLE export for NID: " + importStubEntry.Nid;
|
||||||
Console.Error.WriteLine($"[LOADER][WARN] Import#{num} unresolved: nid={importStubEntry.Nid} ret=0x{num7:X16}");
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Import#{num} unresolved: nid={importStubEntry.Nid} ret=0x{num7:X16} " +
|
||||||
|
$"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} r8=0x{num5:X16} r9=0x{num6:X16}");
|
||||||
if (importStubEntry.Nid == "L-Q3LEjIbgA")
|
if (importStubEntry.Nid == "L-Q3LEjIbgA")
|
||||||
{
|
{
|
||||||
string value18 = string.Join(" ", importStubEntry.Nid.Select(delegate (char c)
|
string value18 = string.Join(" ", importStubEntry.Nid.Select(delegate (char c)
|
||||||
@@ -337,9 +455,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
else if (orbisGen2Result != OrbisGen2Result.ORBIS_GEN2_OK)
|
else if (orbisGen2Result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
if (ShouldLogImportResult(importStubEntry.Nid, orbisGen2Result))
|
||||||
$"[LOADER][WARN] Import#{num} result: {orbisGen2Result} ({importStubEntry.Nid}) " +
|
{
|
||||||
$"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} ret=0x{num7:X16}");
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Import#{num} result: {orbisGen2Result} ({importStubEntry.Nid}) " +
|
||||||
|
$"rdi=0x{value:X16} rsi=0x{value2:X16} rdx=0x{num3:X16} rcx=0x{num4:X16} ret=0x{num7:X16}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
cpuContext[CpuRegister.Rbx] = value3;
|
cpuContext[CpuRegister.Rbx] = value3;
|
||||||
cpuContext[CpuRegister.Rbp] = value4;
|
cpuContext[CpuRegister.Rbp] = value4;
|
||||||
@@ -349,11 +470,36 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.R15] = value8;
|
cpuContext[CpuRegister.R15] = value8;
|
||||||
cpuContext[CpuRegister.Rdi] = value;
|
cpuContext[CpuRegister.Rdi] = value;
|
||||||
cpuContext[CpuRegister.Rsi] = value2;
|
cpuContext[CpuRegister.Rsi] = value2;
|
||||||
if (GuestThreadExecution.TryConsumeCurrentEntryExit(out var exitStatus, out var exitReason))
|
if (GuestThreadExecution.TryConsumeCurrentContextTransfer(out var transferTarget))
|
||||||
{
|
{
|
||||||
if (TryCompleteGuestEntryToHostStub(argPackPtr, num, num7, importStubEntry.Nid, exitReason, exitStatus))
|
if (!TryPrepareGuestContextTransfer(
|
||||||
|
transferTarget,
|
||||||
|
out var transferFrame,
|
||||||
|
out var transferStub,
|
||||||
|
out var transferError))
|
||||||
{
|
{
|
||||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)exitStatus);
|
LastError = transferError ?? "failed to prepare guest context transfer";
|
||||||
|
ActiveForcedGuestExit = true;
|
||||||
|
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
||||||
|
return cpuContext[CpuRegister.Rax];
|
||||||
|
}
|
||||||
|
|
||||||
|
*(ulong*)(argPackPtr + 96) = unchecked((ulong)transferStub);
|
||||||
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] fiber.context-transfer rip=0x{transferTarget.Rip:X16} " +
|
||||||
|
$"rsp=0x{transferTarget.Rsp:X16} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} " +
|
||||||
|
$"fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return unchecked((ulong)transferFrame);
|
||||||
|
}
|
||||||
|
if (GuestThreadExecution.TryConsumeCurrentEntryExit(out var exitValue, out var exitReason))
|
||||||
|
{
|
||||||
|
if (TryCompleteGuestEntryToHostStub(argPackPtr, num, num7, importStubEntry.Nid, exitReason, exitValue))
|
||||||
|
{
|
||||||
|
cpuContext[CpuRegister.Rax] = exitValue;
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -361,9 +507,27 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (GuestThreadExecution.TryConsumeCurrentThreadBlock(out var blockReason) &&
|
if (GuestThreadExecution.TryConsumeCurrentThreadBlock(
|
||||||
|
out var blockReason,
|
||||||
|
out var blockContinuation,
|
||||||
|
out var hasBlockContinuation,
|
||||||
|
out var blockWakeKey,
|
||||||
|
out var blockResumeHandler,
|
||||||
|
out var blockWakeHandler,
|
||||||
|
out var blockDeadlineTimestamp) &&
|
||||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
|
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, blockReason))
|
||||||
{
|
{
|
||||||
|
if (hasBlockContinuation)
|
||||||
|
{
|
||||||
|
RegisterBlockedGuestThreadContinuation(
|
||||||
|
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||||
|
blockContinuation,
|
||||||
|
blockWakeKey,
|
||||||
|
blockResumeHandler,
|
||||||
|
blockWakeHandler,
|
||||||
|
blockDeadlineTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
cpuContext[CpuRegister.Rax] = 0uL;
|
cpuContext[CpuRegister.Rax] = 0uL;
|
||||||
}
|
}
|
||||||
if (flag || flag2 || flag3)
|
if (flag || flag2 || flag3)
|
||||||
@@ -374,16 +538,384 @@ 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)
|
||||||
{
|
{
|
||||||
LastError = $"HLE dispatch error for {importStubEntry.Nid}: {ex.GetType().Name}: {ex.Message}";
|
LastError = $"HLE dispatch error for {importStubEntry.Nid}: {ex.GetType().Name}: {ex.Message}";
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] {LastError}");
|
||||||
|
Console.Error.WriteLine($"[LOADER][ERROR] {ex.StackTrace}");
|
||||||
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
||||||
return 18446744071562199298uL;
|
return 18446744071562199298uL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryDispatchLeafImport(
|
||||||
|
CpuContext cpuContext,
|
||||||
|
ImportStubEntry importStubEntry,
|
||||||
|
nint argPackPtr,
|
||||||
|
long dispatchIndex,
|
||||||
|
out ulong result)
|
||||||
|
{
|
||||||
|
result = 0;
|
||||||
|
if (importStubEntry.Export is not { } export ||
|
||||||
|
(export.Target & cpuContext.TargetGeneration) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var arg0 = *(ulong*)argPackPtr;
|
||||||
|
var returnRip = *(ulong*)(argPackPtr + 96);
|
||||||
|
cpuContext.Rip = importStubEntry.Address;
|
||||||
|
cpuContext[CpuRegister.Rdi] = arg0;
|
||||||
|
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
|
||||||
|
cpuContext[CpuRegister.Rdx] = *(ulong*)(argPackPtr + 16);
|
||||||
|
cpuContext[CpuRegister.Rcx] = *(ulong*)(argPackPtr + 24);
|
||||||
|
cpuContext[CpuRegister.R8] = *(ulong*)(argPackPtr + 32);
|
||||||
|
cpuContext[CpuRegister.R9] = *(ulong*)(argPackPtr + 40);
|
||||||
|
cpuContext[CpuRegister.Rbx] = *(ulong*)(argPackPtr + 48);
|
||||||
|
cpuContext[CpuRegister.Rbp] = *(ulong*)(argPackPtr + 56);
|
||||||
|
cpuContext[CpuRegister.R12] = *(ulong*)(argPackPtr + 64);
|
||||||
|
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
|
||||||
|
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
||||||
|
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
||||||
|
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
||||||
|
|
||||||
|
if (_activeGuestThreadState is { } activeGuestThreadState)
|
||||||
|
{
|
||||||
|
Interlocked.Increment(ref activeGuestThreadState.ImportCount);
|
||||||
|
Volatile.Write(ref activeGuestThreadState.LastImportNid, importStubEntry.Nid);
|
||||||
|
Volatile.Write(ref activeGuestThreadState.LastReturnRip, returnRip);
|
||||||
|
}
|
||||||
|
if (dispatchIndex % 100000 == 0)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] Import#{dispatchIndex}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid}) " +
|
||||||
|
$"rdi=0x{arg0:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} " +
|
||||||
|
$"rdx=0x{cpuContext[CpuRegister.Rdx]:X16} rcx=0x{cpuContext[CpuRegister.Rcx]:X16} " +
|
||||||
|
$"ret=0x{returnRip:X16}");
|
||||||
|
}
|
||||||
|
|
||||||
|
int returnValue;
|
||||||
|
if (IsNoBlockLeafImport(importStubEntry.Nid))
|
||||||
|
{
|
||||||
|
cpuContext.ClearRaxWriteFlag();
|
||||||
|
returnValue = export.Function(cpuContext);
|
||||||
|
if (!cpuContext.WasRaxWritten)
|
||||||
|
{
|
||||||
|
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var previousImportCallFrame = GuestThreadExecution.EnterImportCallFrame(
|
||||||
|
returnRip,
|
||||||
|
(ulong)argPackPtr + 104uL,
|
||||||
|
ActiveGuestReturnSlotAddress);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
cpuContext.ClearRaxWriteFlag();
|
||||||
|
returnValue = export.Function(cpuContext);
|
||||||
|
if (!cpuContext.WasRaxWritten)
|
||||||
|
{
|
||||||
|
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
var returnResult = (OrbisGen2Result)returnValue;
|
||||||
|
if (ShouldLogImportResult(importStubEntry.Nid, returnResult))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Import#{dispatchIndex} result: {returnResult} ({importStubEntry.Nid}) " +
|
||||||
|
$"rdi=0x{arg0:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} " +
|
||||||
|
$"rdx=0x{cpuContext[CpuRegister.Rdx]:X16} rcx=0x{cpuContext[CpuRegister.Rcx]:X16} " +
|
||||||
|
$"r8=0x{cpuContext[CpuRegister.R8]:X16} r9=0x{cpuContext[CpuRegister.R9]:X16} " +
|
||||||
|
$"ret=0x{returnRip:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GuestThreadExecution.TryConsumeCurrentThreadBlock(
|
||||||
|
out var blockReason,
|
||||||
|
out var blockContinuation,
|
||||||
|
out var hasBlockContinuation,
|
||||||
|
out var blockWakeKey,
|
||||||
|
out var blockResumeHandler,
|
||||||
|
out var blockWakeHandler,
|
||||||
|
out var blockDeadlineTimestamp) &&
|
||||||
|
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
|
||||||
|
{
|
||||||
|
if (hasBlockContinuation)
|
||||||
|
{
|
||||||
|
RegisterBlockedGuestThreadContinuation(
|
||||||
|
GuestThreadExecution.CurrentGuestThreadHandle,
|
||||||
|
blockContinuation,
|
||||||
|
blockWakeKey,
|
||||||
|
blockResumeHandler,
|
||||||
|
blockWakeHandler,
|
||||||
|
blockDeadlineTimestamp);
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuContext[CpuRegister.Rax] = 0uL;
|
||||||
|
}
|
||||||
|
|
||||||
|
result = cpuContext[CpuRegister.Rax];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Subset of the leaf set that additionally skips the import-call-frame
|
||||||
|
// bookkeeping. The same scalar-only (no XMM args, no XMM return) constraint
|
||||||
|
// as IsLeafImport applies — see the note there before adding entries.
|
||||||
|
// NOTE: this filter is only consulted after IsLeafImport accepts the NID;
|
||||||
|
// entries listed here but not in IsLeafImport (scePadRead, scePadOpen,
|
||||||
|
// sceUserServiceGetEvent, sceUserServiceGetPlatformPrivacySetting,
|
||||||
|
// pthread_mutex_trylock) currently take the full gateway path.
|
||||||
|
private static bool IsNoBlockLeafImport(string nid) =>
|
||||||
|
nid is
|
||||||
|
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||||
|
"a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
|
||||||
|
"Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
|
||||||
|
"GuchCTefuZw" or // sceAmprCommandBufferDestructor
|
||||||
|
"N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
|
||||||
|
"baQO9ez2gL4" or // sceAmprCommandBufferReset
|
||||||
|
"ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
|
||||||
|
"mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
|
||||||
|
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
|
||||||
|
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
|
||||||
|
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
|
||||||
|
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
|
||||||
|
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
|
||||||
|
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
|
||||||
|
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
|
||||||
|
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||||
|
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
||||||
|
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||||
|
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||||
|
"Q2V+iqvjgC0" or // vsnprintf
|
||||||
|
"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)
|
||||||
|
{
|
||||||
|
var resultValue = unchecked((int)result);
|
||||||
|
if (resultValue > 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var expectedFileProbeMiss =
|
||||||
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND &&
|
||||||
|
IsExpectedFileProbeNotFoundNid(nid);
|
||||||
|
var expectedTimedWaitTimeout =
|
||||||
|
string.Equals(nid, "27bAgiJmOh0", StringComparison.Ordinal) &&
|
||||||
|
unchecked((int)result) == 60;
|
||||||
|
var expectedEqueueTimeout =
|
||||||
|
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
|
||||||
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
|
var expectedMutexTrylockBusy =
|
||||||
|
string.Equals(nid, "K-jXhbt2gn4", StringComparison.Ordinal) &&
|
||||||
|
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
|
var expectedUserServiceNoEvent =
|
||||||
|
string.Equals(nid, "yH17Q6NWtVg", StringComparison.Ordinal) &&
|
||||||
|
resultValue == unchecked((int)0x80960007);
|
||||||
|
var expectedPrivacyInvalidParameter =
|
||||||
|
string.Equals(nid, "D-CzAxQL0XI", StringComparison.Ordinal) &&
|
||||||
|
resultValue == unchecked((int)0x80960009);
|
||||||
|
if (!expectedFileProbeMiss &&
|
||||||
|
!expectedTimedWaitTimeout &&
|
||||||
|
!expectedEqueueTimeout &&
|
||||||
|
!expectedMutexTrylockBusy &&
|
||||||
|
!expectedUserServiceNoEvent &&
|
||||||
|
!expectedPrivacyInvalidParameter)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ShouldLogExpectedImportResults())
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var key = nid + "\0" + resultValue;
|
||||||
|
int count;
|
||||||
|
lock (_importResultLogSampleGate)
|
||||||
|
{
|
||||||
|
_importResultLogSamples.TryGetValue(key, out count);
|
||||||
|
count++;
|
||||||
|
_importResultLogSamples[key] = count;
|
||||||
|
}
|
||||||
|
|
||||||
|
return count <= 8 || count % 10000 == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldLogExpectedImportResults() =>
|
||||||
|
string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EXPECTED_IMPORT_RESULTS"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private static bool IsExpectedFileProbeNotFoundNid(string nid) =>
|
||||||
|
nid is
|
||||||
|
"eV9wAD2riIA" or // sceKernelStat
|
||||||
|
"1G3lF1Gg1k8" or // sceKernelOpen
|
||||||
|
"gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes
|
||||||
|
|
||||||
|
// LEAF-IMPORT REGISTRATION — scalar-only constraint.
|
||||||
|
//
|
||||||
|
// TryDispatchLeafImport is a fast path that never copies the trampoline's XMM
|
||||||
|
// save area into CpuContext and never publishes an XMM0 return back to the
|
||||||
|
// guest (see DispatchImport). Every NID listed here must therefore satisfy BOTH:
|
||||||
|
// 1. no float/double parameters (nothing passed in xmm0..xmm7), and
|
||||||
|
// 2. no float/double return value (nothing returned in xmm0).
|
||||||
|
// Integer/pointer arguments and returns only. va_list-based functions
|
||||||
|
// (vsnprintf) are safe: SysV va_list floats are read from the caller-built
|
||||||
|
// reg_save_area in guest memory, not from the callee's incoming XMM registers.
|
||||||
|
//
|
||||||
|
// If a function that consumes XMM arguments or returns in xmm0 (e.g. powf,
|
||||||
|
// logf, wcstod, sceVideoOutColorSettingsSetGamma_) is ever added here, its
|
||||||
|
// float arguments will silently arrive stale and its return value will be
|
||||||
|
// dropped. Such functions must stay on the full gateway path — do not list them.
|
||||||
|
//
|
||||||
|
// Audited 2026-07-11 (PR #59): every NID below maps to a registered
|
||||||
|
// SysAbiExport whose handler neither reads nor writes CpuContext XMM state
|
||||||
|
// and whose known guest signature is integer/pointer only.
|
||||||
|
private bool IsLeafImport(string nid)
|
||||||
|
{
|
||||||
|
if (nid == "1jfXLRVzisc") // sceKernelUsleep
|
||||||
|
{
|
||||||
|
return !_logUsleep;
|
||||||
|
}
|
||||||
|
|
||||||
|
return nid is
|
||||||
|
"9UK1vLZQft4" or // scePthreadMutexLock
|
||||||
|
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||||
|
"7H0iTOciTLo" or // pthread_mutex_lock
|
||||||
|
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||||
|
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||||
|
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||||
|
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||||
|
"qj7QZpgr9Uw" or // sceAgcUnknownQj7QZpgr9Uw (unknown NID; observed scalar: command-buffer pointer in, packet address out)
|
||||||
|
"LtTouSCZjHM" or // sceAgcCbNop
|
||||||
|
"k3GhuSNmBLU" or // sceAgcCbDispatch
|
||||||
|
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
|
||||||
|
"JrtiDtKeS38" or // sceAgcAcbResetQueue
|
||||||
|
"cFazmnXpJOE" or // sceAgcAcbEventWrite
|
||||||
|
"KT-hTp-Ch14" or // sceAgcAcbAcquireMem
|
||||||
|
"htn36gPnBk4" or // sceAgcAcbWaitRegMem
|
||||||
|
"eZ4+17OQz4Q" or // sceAgcAcbWriteData
|
||||||
|
"j3EtxFkSIhQ" or // sceAgcAcbDispatchIndirect
|
||||||
|
"gSRnr79F8tQ" or // sceAgcDriverSubmitAcb
|
||||||
|
"i1jyy49AjXU" or // sceAgcDcbWriteData
|
||||||
|
"VmW0Tdpy420" or // sceAgcDcbWaitRegMem
|
||||||
|
"WmAc2MEj6Io" or // sceAgcDcbDmaData
|
||||||
|
"rUuVjyR+Rd4" or // sceAgcDcbGetLodStatsGetSize
|
||||||
|
"vuSXe69VILM" or // sceAgcDcbGetLodStats
|
||||||
|
"RmaJwLtc8rY" or // sceAgcDcbSetBaseIndirectArgs
|
||||||
|
"CtB+A9-VxO0" or // sceAgcDcbDispatchIndirect
|
||||||
|
"+kSrjIVxKFE" or // sceAgcDcbPushMarker
|
||||||
|
"H7uZqCoNuWk" or // sceAgcDcbPopMarker
|
||||||
|
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
|
||||||
|
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
|
||||||
|
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
|
||||||
|
"a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
|
||||||
|
"Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
|
||||||
|
"GuchCTefuZw" or // sceAmprCommandBufferDestructor
|
||||||
|
"N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
|
||||||
|
"baQO9ez2gL4" or // sceAmprCommandBufferReset
|
||||||
|
"ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
|
||||||
|
"mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
|
||||||
|
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
|
||||||
|
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
|
||||||
|
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
|
||||||
|
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
|
||||||
|
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
|
||||||
|
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
|
||||||
|
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
|
||||||
|
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||||
|
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
||||||
|
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||||
|
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||||
|
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
|
||||||
|
"TywrFKCoLGY" or // sceSaveDataInitialize3
|
||||||
|
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
|
||||||
|
"ZP4e7rlzOUk" or // sceSaveDataMount3
|
||||||
|
"ERKzksauAJA" or // sceSaveDataDialogGetStatus
|
||||||
|
"KK3Bdg1RWK0" or // sceSaveDataDialogUpdateStatus
|
||||||
|
"en7gNVnh878" or // sceSaveDataDialogIsReadyToDisplay
|
||||||
|
"jO8DM8oyego" or // sceNpEntitlementAccessInitialize
|
||||||
|
"TFyU+KFBv54" or // sceNpEntitlementAccessGetAddcontEntitlementInfoList
|
||||||
|
"27bAgiJmOh0" or // pthread_cond_timedwait
|
||||||
|
"iQw3iQPhvUQ" or // sceNetCtlCheckCallback
|
||||||
|
"Q2V+iqvjgC0" or // vsnprintf
|
||||||
|
"j4ViWNHEgww" or // strlen
|
||||||
|
"5jNubw4vlAA" or // strnlen
|
||||||
|
"LHMrG7e8G78" or // wcslen
|
||||||
|
"WkkeywLJcgU" or // wcslen
|
||||||
|
"Ovb2dSJOAuE" or // strcmp
|
||||||
|
"aesyjrHVWy4" or // strncmp
|
||||||
|
"pNtJdE3x49E" or // wcscmp
|
||||||
|
"fV2xHER+bKE" or // wcscoll
|
||||||
|
"E8wCoUEbfzk" or // wcsncmp
|
||||||
|
"Q3VBxCXhUHs" or // memcpy
|
||||||
|
"+P6FRGH4LfA" or // memmove
|
||||||
|
"DfivPArhucg" or // memcmp
|
||||||
|
"ytQULN-nhL4" or // pthread_rwlock_init
|
||||||
|
"6ULAa0fq4jA" or // scePthreadRwlockInit
|
||||||
|
"1471ajPzxh0" or // pthread_rwlock_destroy
|
||||||
|
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
|
||||||
|
"iGjsr1WAtI0" or // pthread_rwlock_rdlock
|
||||||
|
"Ox9i0c7L5w0" or // scePthreadRwlockRdlock
|
||||||
|
"sIlRvQqsN2Y" or // pthread_rwlock_wrlock
|
||||||
|
"mqdNorrB+gI" or // scePthreadRwlockWrlock
|
||||||
|
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||||
|
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
||||||
|
"aI+OeCz8xrQ" or // scePthreadSelf
|
||||||
|
"EotR8a3ASf4" or // pthread_self
|
||||||
|
"eoht7mQOCmo" or // scePthreadGetspecific
|
||||||
|
"0-KXaS70xy4" or // pthread_getspecific
|
||||||
|
"+BzXYkqYeLE" or // scePthreadSetspecific
|
||||||
|
"WrOLvHU0yQM" or // pthread_setspecific
|
||||||
|
"vz+pg2zdopI" or // sceKernelGetEventUserData
|
||||||
|
"mJ7aghmgvfc" or // sceKernelGetEventId
|
||||||
|
"23CPPI1tyBY" or // sceKernelGetEventFilter
|
||||||
|
"kwGyyjohI50" or // sceKernelGetEventData
|
||||||
|
// _Getpctype is a per-character ctype table lookup; the embedded mcpp preprocessor
|
||||||
|
// re-fetches the table pointer on every isdigit()/isalpha() check, so classifying a
|
||||||
|
// large input legitimately calls it hundreds of thousands of times back-to-back -
|
||||||
|
// real, finite work that would otherwise trip the loop guard.
|
||||||
|
"sUP1hBaouOw"; // _Getpctype
|
||||||
|
}
|
||||||
|
|
||||||
|
private long NextImportDispatchIndex()
|
||||||
|
{
|
||||||
|
if (!ReferenceEquals(_importCounterOwner, this) ||
|
||||||
|
_nextImportDispatchIndex >= _importDispatchBlockEnd)
|
||||||
|
{
|
||||||
|
var blockEnd = Interlocked.Add(ref _importDispatchCount, ImportDispatchBlockSize);
|
||||||
|
_importCounterOwner = this;
|
||||||
|
_nextImportDispatchIndex = blockEnd - ImportDispatchBlockSize + 1;
|
||||||
|
_importDispatchBlockEnd = blockEnd + 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return _nextImportDispatchIndex++;
|
||||||
|
}
|
||||||
|
|
||||||
private void TraceImportFrameChain(CpuContext context, long dispatchIndex)
|
private void TraceImportFrameChain(CpuContext context, long dispatchIndex)
|
||||||
{
|
{
|
||||||
var frame = context[CpuRegister.Rbp];
|
var frame = context[CpuRegister.Rbp];
|
||||||
@@ -431,7 +963,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, int status)
|
private unsafe bool TryCompleteGuestEntryToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid, string reason, ulong value)
|
||||||
{
|
{
|
||||||
ulong hostExit = ActiveEntryReturnSentinelRip;
|
ulong hostExit = ActiveEntryReturnSentinelRip;
|
||||||
if (hostExit < 65536 || !TryPatchActiveGuestReturnSlot(hostExit))
|
if (hostExit < 65536 || !TryPatchActiveGuestReturnSlot(hostExit))
|
||||||
@@ -447,7 +979,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][INFO] Guest entry exit at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={reason} status={status}");
|
$"[LOADER][INFO] Guest entry exit at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={reason} value=0x{value:X16}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -469,8 +1001,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
ActiveGuestThreadYieldRequested = true;
|
ActiveGuestThreadYieldRequested = true;
|
||||||
ActiveGuestThreadYieldReason = string.IsNullOrWhiteSpace(reason) ? nid : reason;
|
ActiveGuestThreadYieldReason = string.IsNullOrWhiteSpace(reason) ? nid : reason;
|
||||||
Console.Error.WriteLine(
|
if (_logGuestThreads)
|
||||||
$"[LOADER][INFO] Guest thread yield at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={ActiveGuestThreadYieldReason}");
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Guest thread yield at import#{dispatchIndex}: nid={nid} ret=0x{returnRip:X16} reason={ActiveGuestThreadYieldReason}");
|
||||||
|
}
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -488,7 +1023,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"), "1", StringComparison.Ordinal))
|
if (_disableImportLoopGuard || _importLoopGuardSeconds <= 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -503,6 +1038,10 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_importNidHashCache[nid] = value;
|
_importNidHashCache[nid] = value;
|
||||||
}
|
}
|
||||||
RecordImportLoopSignature(value, returnRip, BuildImportLoopSignature(value, returnRip, arg0, arg1));
|
RecordImportLoopSignature(value, returnRip, BuildImportLoopSignature(value, returnRip, arg0, arg1));
|
||||||
|
if ((dispatchIndex & 0x3F) != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
if (!HasRepeatingImportLoopPattern())
|
if (!HasRepeatingImportLoopPattern())
|
||||||
{
|
{
|
||||||
if (_importLoopPatternHits > 0)
|
if (_importLoopPatternHits > 0)
|
||||||
@@ -520,14 +1059,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
_importLoopPatternStartTimestamp = Stopwatch.GetTimestamp();
|
_importLoopPatternStartTimestamp = Stopwatch.GetTimestamp();
|
||||||
}
|
}
|
||||||
_importLoopPatternHits++;
|
_importLoopPatternHits++;
|
||||||
var guardSeconds = GetImportLoopGuardSeconds();
|
if (_importLoopPatternHits < 6)
|
||||||
if (guardSeconds <= 0 || _importLoopPatternHits < 6)
|
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var elapsedTicks = Stopwatch.GetTimestamp() - _importLoopPatternStartTimestamp;
|
var elapsedTicks = Stopwatch.GetTimestamp() - _importLoopPatternStartTimestamp;
|
||||||
return elapsedTicks >= (long)(guardSeconds * Stopwatch.Frequency);
|
return elapsedTicks >= (long)(_importLoopGuardSeconds * Stopwatch.Frequency);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsImportLoopGuardBoundary(string nid) =>
|
private static bool IsImportLoopGuardBoundary(string nid) =>
|
||||||
@@ -778,8 +1316,11 @@ 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;
|
||||||
}
|
}
|
||||||
@@ -792,6 +1333,62 @@ 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;
|
||||||
@@ -810,8 +1407,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
bool logBootstrap = string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_BOOTSTRAP"), "1", StringComparison.Ordinal);
|
if (_logBootstrap)
|
||||||
if (logBootstrap)
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
|
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
|
||||||
|
|||||||
@@ -0,0 +1,582 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,11 +10,13 @@ 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;
|
||||||
@@ -155,7 +157,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
|
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
|
||||||
Console.Error.WriteLine($"[LOADER][TLS] load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
|
Log.Debug($"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);
|
||||||
@@ -255,7 +257,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
_nextTlsModuleId++;
|
_nextTlsModuleId++;
|
||||||
}
|
}
|
||||||
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||||
|
|
||||||
return new SelfImage(
|
return new SelfImage(
|
||||||
loadContext.IsSelf,
|
loadContext.IsSelf,
|
||||||
@@ -403,15 +405,15 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
var virtualAddress = header.VirtualAddress + imageBase;
|
var virtualAddress = header.VirtualAddress + imageBase;
|
||||||
|
|
||||||
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}");
|
Log.Debug($"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)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Warning(
|
||||||
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
|
$"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}");
|
||||||
}
|
}
|
||||||
@@ -671,13 +673,13 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||||
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,8 +691,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
||||||
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -783,15 +785,15 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
$"[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))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocation.Type}");
|
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -800,7 +802,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
|
Log.Debug("[FOCUS][SKIP] target address not mapped");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -840,7 +842,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -852,10 +854,9 @@ 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)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Warning(
|
||||||
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
$"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(
|
||||||
@@ -873,10 +874,9 @@ 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)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Warning(
|
||||||
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
$"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)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
|
Log.Debug($"[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)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
Log.Debug($"[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)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
$"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)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
$"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);
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
$"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;
|
||||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
||||||
if (virtualMemory.TryRead(guestAddr, tableBytes))
|
if (virtualMemory.TryRead(guestAddr, tableBytes))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (virtualMemory.TryRead(location, tableBytes))
|
if (virtualMemory.TryRead(location, tableBytes))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
Log.Debug($"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();
|
||||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
|
Log.Warning($"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)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TEST] ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
||||||
|
|
||||||
if (address == 0)
|
if (address == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
|
Log.Debug("-> 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))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TEST] -> resolved raw 0x{resolved:X16}");
|
Log.Debug($"-> 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))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TEST] -> resolved rebased 0x{resolvedRebased:X16}");
|
Log.Debug($"-> resolved rebased 0x{resolvedRebased:X16}");
|
||||||
return resolvedRebased;
|
return resolvedRebased;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (address < 0x10000)
|
if (address < 0x10000)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
|
Log.Debug($"-> reject small 0x{address:X}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
|
Log.Debug($"-> fallback raw 0x{address:X}");
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,14 +4,19 @@
|
|||||||
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 readonly object _gate = new();
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
||||||
|
|
||||||
|
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;
|
||||||
@@ -19,7 +24,8 @@ 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 LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
|
private const ulong FullCommitRegionLimit = 4UL << 30;
|
||||||
|
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;
|
||||||
@@ -35,6 +41,7 @@ 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);
|
||||||
@@ -51,7 +58,11 @@ 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 FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
private static extern void* GetCurrentProcess();
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||||
|
|
||||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||||
{
|
{
|
||||||
@@ -78,9 +89,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_gate)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_regions.Add(new MemoryRegion
|
InsertRegionSorted(new MemoryRegion
|
||||||
{
|
{
|
||||||
VirtualAddress = actualAddress,
|
VirtualAddress = actualAddress,
|
||||||
Size = alignedSize,
|
Size = alignedSize,
|
||||||
@@ -89,9 +101,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Protection = protection
|
Protection = protection
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
var allocationKind = executable ? "executable memory" : "data memory";
|
var allocationKind = executable ? "executable memory" : "data memory";
|
||||||
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
TraceVmem($"Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,7 +121,9 @@ 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 && alignedSize >= LargeDataReserveThreshold;
|
var preferReserveOnly = !executable &&
|
||||||
|
alignedSize >= LargeDataReserveThreshold &&
|
||||||
|
alignedSize > FullCommitRegionLimit;
|
||||||
|
|
||||||
void* result = null;
|
void* result = null;
|
||||||
if (preferReserveOnly)
|
if (preferReserveOnly)
|
||||||
@@ -134,7 +152,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)");
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[VMEM] Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
TraceVmem($"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)
|
||||||
@@ -188,12 +206,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}";
|
||||||
Console.Error.WriteLine($"[VMEM] Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
TraceVmem($"Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
lazyPrimeState = $"fail:{primeBytes:X}";
|
lazyPrimeState = $"fail:{primeBytes:X}";
|
||||||
Console.Error.WriteLine($"[VMEM] Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
TraceVmem($"Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
@@ -202,9 +220,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (_gate)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
_regions.Add(new MemoryRegion
|
InsertRegionSorted(new MemoryRegion
|
||||||
{
|
{
|
||||||
VirtualAddress = actualAddress,
|
VirtualAddress = actualAddress,
|
||||||
Size = alignedSize,
|
Size = alignedSize,
|
||||||
@@ -213,11 +232,15 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
Protection = protection
|
Protection = protection
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
|
||||||
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");
|
||||||
Console.Error.WriteLine($"[VMEM] Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
|
TraceVmem($"Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
|
||||||
|
|
||||||
return actualAddress;
|
return actualAddress;
|
||||||
}
|
}
|
||||||
@@ -237,7 +260,8 @@ 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 cursor = AlignUp(desiredAddress, effectiveAlignment);
|
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||||
|
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||||
|
|
||||||
for (var attempt = 0; attempt < 0x10000; attempt++)
|
for (var attempt = 0; attempt < 0x10000; attempt++)
|
||||||
{
|
{
|
||||||
@@ -257,6 +281,7 @@ 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;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -315,7 +340,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
{
|
{
|
||||||
lock (_guestAllocationGate)
|
lock (_guestAllocationGate)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
foreach (var region in _regions)
|
||||||
{
|
{
|
||||||
@@ -323,6 +349,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
_regions.Clear();
|
_regions.Clear();
|
||||||
_pageProtections.Clear();
|
_pageProtections.Clear();
|
||||||
|
lock (_allocationSearchHintGate)
|
||||||
|
{
|
||||||
|
_allocationSearchHints.Clear();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
}
|
}
|
||||||
|
|
||||||
_guestAllocationArenaBase = 0;
|
_guestAllocationArenaBase = 0;
|
||||||
@@ -343,7 +377,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var mapEnd = AlignUp(segmentEnd, PageSize);
|
var mapEnd = AlignUp(segmentEnd, PageSize);
|
||||||
var mapSize = checked(mapEnd - mapStart);
|
var mapSize = checked(mapEnd - mapStart);
|
||||||
|
|
||||||
lock (_gate)
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var existingRegion = FindRegion(mapStart, mapSize);
|
var existingRegion = FindRegion(mapStart, mapSize);
|
||||||
if (existingRegion == null)
|
if (existingRegion == null)
|
||||||
@@ -374,7 +409,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
ApplySegmentProtection(mapStart, mapEnd, protection);
|
ApplySegmentProtection(mapStart, mapEnd, protection);
|
||||||
|
|
||||||
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
TraceVmem($"Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -419,13 +458,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||||
{
|
{
|
||||||
FlushInstructionCache(null, (void*)address, (nuint)size);
|
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
||||||
{
|
{
|
||||||
lock (_gate)
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
||||||
for (var i = 0; i < _regions.Count; i++)
|
for (var i = 0; i < _regions.Count; i++)
|
||||||
@@ -440,96 +480,240 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
return snapshot;
|
return snapshot;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
public bool TryRead(ulong virtualAddress, Span<byte> destination)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
var requiresExclusiveAccess = false;
|
||||||
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||||
|
if (region is not null &&
|
||||||
|
TryResolveRegionOffset(
|
||||||
|
virtualAddress,
|
||||||
|
(ulong)destination.Length,
|
||||||
|
region,
|
||||||
|
out var offset))
|
||||||
{
|
{
|
||||||
if (TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
if (destination.IsEmpty)
|
||||||
{
|
{
|
||||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
return true;
|
||||||
if (destination.IsEmpty)
|
}
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (region.IsReservedOnly)
|
||||||
|
{
|
||||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
|
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
|
{
|
||||||
|
requiresExclusiveAccess = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fixed (byte* destPtr = destination)
|
||||||
{
|
{
|
||||||
return false;
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
fixed (byte* destPtr = destination)
|
|
||||||
{
|
|
||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
RestorePageProtections(touchedPages);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requiresExclusiveAccess)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return TryReadExclusive(virtualAddress, destination);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
var requiresExclusiveAccess = false;
|
||||||
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
var region = FindRegion(virtualAddress, (ulong)source.Length);
|
||||||
|
if (region is not null &&
|
||||||
|
TryResolveRegionOffset(
|
||||||
|
virtualAddress,
|
||||||
|
(ulong)source.Length,
|
||||||
|
region,
|
||||||
|
out var offset))
|
||||||
{
|
{
|
||||||
if (TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
if (source.IsEmpty)
|
||||||
{
|
{
|
||||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
return true;
|
||||||
if (source.IsEmpty)
|
}
|
||||||
{
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
|
if (region.IsReservedOnly)
|
||||||
|
{
|
||||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||||
|
{
|
||||||
|
requiresExclusiveAccess = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fixed (byte* srcPtr = source)
|
||||||
{
|
{
|
||||||
return false;
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
}
|
|
||||||
|
|
||||||
try
|
|
||||||
{
|
|
||||||
fixed (byte* srcPtr = source)
|
|
||||||
{
|
|
||||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
finally
|
|
||||||
{
|
|
||||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
|
||||||
if (IsExecutableProtection(oldProtect))
|
|
||||||
{
|
|
||||||
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!requiresExclusiveAccess)
|
||||||
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_gate.EnterWriteLock();
|
||||||
|
try
|
||||||
|
{
|
||||||
|
return TryWriteExclusive(virtualAddress, source);
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitWriteLock();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||||
|
{
|
||||||
|
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||||
|
if (region is not null &&
|
||||||
|
TryResolveRegionOffset(
|
||||||
|
virtualAddress,
|
||||||
|
(ulong)destination.Length,
|
||||||
|
region,
|
||||||
|
out var offset))
|
||||||
|
{
|
||||||
|
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||||
|
{
|
||||||
|
fixed (byte* destPtr = destination)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryTemporarilyProtectForRead((ulong)srcPtr, (ulong)destination.Length, region, out var touchedPages))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fixed (byte* destPtr = destination)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
RestorePageProtections(touchedPages);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||||
|
{
|
||||||
|
var region = FindRegion(virtualAddress, (ulong)source.Length);
|
||||||
|
if (region is not null &&
|
||||||
|
TryResolveRegionOffset(
|
||||||
|
virtualAddress,
|
||||||
|
(ulong)source.Length,
|
||||||
|
region,
|
||||||
|
out var offset))
|
||||||
|
{
|
||||||
|
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||||
|
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||||
|
{
|
||||||
|
fixed (byte* srcPtr = source)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!VirtualProtect(destPtr, (nuint)source.Length, PAGE_EXECUTE_READWRITE, out var oldProtect))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
fixed (byte* srcPtr = source)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||||
|
if (IsExecutableProtection(oldProtect))
|
||||||
|
{
|
||||||
|
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
|
public bool TryWriteUInt64(ulong virtualAddress, ulong value)
|
||||||
@@ -541,45 +725,76 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
|
|
||||||
public void* GetPointer(ulong virtualAddress)
|
public void* GetPointer(ulong virtualAddress)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
return FindRegion(virtualAddress, 1) is not null
|
||||||
{
|
? (void*)virtualAddress
|
||||||
if (virtualAddress >= region.VirtualAddress &&
|
: null;
|
||||||
virtualAddress < region.VirtualAddress + region.Size)
|
}
|
||||||
{
|
finally
|
||||||
return (void*)virtualAddress;
|
{
|
||||||
}
|
_gate.ExitReadLock();
|
||||||
}
|
|
||||||
return null;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public bool IsAccessible(ulong virtualAddress, ulong size)
|
public bool IsAccessible(ulong virtualAddress, ulong size)
|
||||||
{
|
{
|
||||||
lock (_gate)
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
return FindRegion(virtualAddress, size) is not null;
|
||||||
{
|
}
|
||||||
if (TryResolveRegionOffset(virtualAddress, size, region, out _))
|
finally
|
||||||
{
|
{
|
||||||
return true;
|
_gate.ExitReadLock();
|
||||||
}
|
|
||||||
}
|
|
||||||
return false;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private MemoryRegion? FindRegion(ulong address, ulong size)
|
private MemoryRegion? FindRegion(ulong address, ulong size)
|
||||||
{
|
{
|
||||||
foreach (var region in _regions)
|
var low = 0;
|
||||||
|
var high = _regions.Count - 1;
|
||||||
|
MemoryRegion? candidate = null;
|
||||||
|
while (low <= high)
|
||||||
{
|
{
|
||||||
if (TryResolveRegionOffset(address, size, region, out _))
|
var middle = low + ((high - low) >> 1);
|
||||||
|
var region = _regions[middle];
|
||||||
|
if (region.VirtualAddress <= address)
|
||||||
{
|
{
|
||||||
return region;
|
candidate = 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)
|
||||||
@@ -591,21 +806,67 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
}
|
}
|
||||||
|
|
||||||
var end = address + size;
|
var end = address + size;
|
||||||
lock (_gate)
|
_gate.EnterReadLock();
|
||||||
|
try
|
||||||
{
|
{
|
||||||
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
_gate.ExitReadLock();
|
||||||
|
}
|
||||||
|
|
||||||
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;
|
||||||
@@ -633,6 +894,44 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return protection is PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY;
|
return protection is PAGE_EXECUTE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE or PAGE_EXECUTE_WRITECOPY;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private bool CanReadWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
|
||||||
|
CanAccessWithoutProtectionChange(address, size, region, write: false);
|
||||||
|
|
||||||
|
private bool CanWriteWithoutProtectionChange(ulong address, ulong size, MemoryRegion region) =>
|
||||||
|
CanAccessWithoutProtectionChange(address, size, region, write: true);
|
||||||
|
|
||||||
|
private bool CanAccessWithoutProtectionChange(ulong address, ulong size, MemoryRegion region, bool write)
|
||||||
|
{
|
||||||
|
var startPage = AlignDown(address, PageSize);
|
||||||
|
var endPage = AlignUp(address + size, PageSize);
|
||||||
|
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
||||||
|
{
|
||||||
|
if (_pageProtections.TryGetValue(pageAddress, out var flags))
|
||||||
|
{
|
||||||
|
if (write ? (flags & ProgramHeaderFlags.Write) == 0 : (flags & ProgramHeaderFlags.Read) == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (write ? !IsWritableProtection(region.Protection) : !IsReadableProtection(region.Protection))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsReadableProtection(uint protection)
|
||||||
|
{
|
||||||
|
return protection is PAGE_READONLY or PAGE_READWRITE or PAGE_EXECUTE_READ or PAGE_EXECUTE_READWRITE;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsWritableProtection(uint protection)
|
||||||
|
{
|
||||||
|
return protection is PAGE_READWRITE or PAGE_EXECUTE_READWRITE;
|
||||||
|
}
|
||||||
|
|
||||||
private static uint GetCommitProtection(MemoryRegion region)
|
private static uint GetCommitProtection(MemoryRegion region)
|
||||||
{
|
{
|
||||||
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
return region.IsExecutable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||||
@@ -649,15 +948,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
var endPage = AlignUp(address + size, PageSize);
|
var endPage = AlignUp(address + size, PageSize);
|
||||||
var commitProtection = GetCommitProtection(region);
|
var commitProtection = GetCommitProtection(region);
|
||||||
|
|
||||||
for (var pageAddress = startPage; pageAddress < endPage; pageAddress += PageSize)
|
var pageAddress = startPage;
|
||||||
|
while (pageAddress < endPage)
|
||||||
{
|
{
|
||||||
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
if (VirtualQuery((void*)pageAddress, out var info, (nuint)sizeof(MemoryBasicInformation64)) == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var queriedEnd = info.RegionSize > ulong.MaxValue - info.BaseAddress
|
||||||
|
? ulong.MaxValue
|
||||||
|
: info.BaseAddress + info.RegionSize;
|
||||||
|
var rangeEnd = Math.Min(endPage, queriedEnd);
|
||||||
|
if (rangeEnd <= pageAddress)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
if (info.State == MEM_COMMIT)
|
if (info.State == MEM_COMMIT)
|
||||||
{
|
{
|
||||||
|
pageAddress = rangeEnd;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -666,10 +976,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (VirtualAlloc((void*)pageAddress, (nuint)PageSize, MEM_COMMIT, commitProtection) == null)
|
var commitSize = rangeEnd - pageAddress;
|
||||||
|
if (VirtualAlloc((void*)pageAddress, (nuint)commitSize, MEM_COMMIT, commitProtection) == null)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pageAddress = rangeEnd;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
@@ -722,6 +1035,29 @@ 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)
|
||||||
|
|||||||
@@ -10,15 +10,20 @@ using SharpEmu.HLE;
|
|||||||
using SharpEmu.Libs.VideoOut;
|
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.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)
|
||||||
@@ -127,7 +132,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);
|
||||||
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
Log.Info($"Loading: {ebootPath}");
|
||||||
LastExecutionDiagnostics = null;
|
LastExecutionDiagnostics = null;
|
||||||
LastExecutionTrace = null;
|
LastExecutionTrace = null;
|
||||||
LastSessionSummary = null;
|
LastSessionSummary = null;
|
||||||
@@ -136,9 +141,11 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
KernelModuleRegistry.Reset();
|
KernelModuleRegistry.Reset();
|
||||||
var image = LoadImage(normalizedEbootPath);
|
var image = LoadImage(normalizedEbootPath);
|
||||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||||
|
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);
|
||||||
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
Log.Info($"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);
|
||||||
@@ -161,7 +168,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName);
|
processImageName);
|
||||||
if (initializerResult is { } failedInitializerResult)
|
if (initializerResult is { } failedInitializerResult)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -169,8 +176,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return failedInitializerResult;
|
return failedInitializerResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
Log.Info($"Dispatching, gen: {generation}");
|
||||||
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
@@ -180,8 +187,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
processImageName,
|
processImageName,
|
||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
|
|
||||||
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
Log.Info($"DispatchEntry returned: {result}");
|
||||||
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
Log.Info($"Dispatch result: {result}");
|
||||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||||
@@ -352,6 +359,66 @@ 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";
|
||||||
@@ -432,8 +499,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
moduleName = $"module#{i}";
|
moduleName = $"module#{i}";
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||||
initEntryPoint,
|
initEntryPoint,
|
||||||
@@ -444,8 +511,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
_cpuExecutionOptions);
|
_cpuExecutionOptions);
|
||||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Error(
|
||||||
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
$"Module start failed: {moduleName} -> {result}");
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -466,8 +533,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||||
|
|
||||||
var result = RunInitializerList(
|
var result = RunInitializerList(
|
||||||
$"{label}:preinit",
|
$"{label}:preinit",
|
||||||
@@ -506,8 +573,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||||
|
|
||||||
var result = _cpuDispatcher.DispatchEntry(
|
var result = _cpuDispatcher.DispatchEntry(
|
||||||
initializerAddress,
|
initializerAddress,
|
||||||
@@ -541,6 +608,7 @@ 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)
|
||||||
@@ -552,7 +620,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
}
|
}
|
||||||
|
|
||||||
var allModulePaths = moduleDirectories
|
var allModulePaths = moduleDirectories
|
||||||
.SelectMany(Directory.EnumerateFiles)
|
.SelectMany(directory => Directory
|
||||||
|
.EnumerateFiles(directory)
|
||||||
|
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
||||||
.Where(path =>
|
.Where(path =>
|
||||||
{
|
{
|
||||||
var extension = Path.GetExtension(path);
|
var extension = Path.GetExtension(path);
|
||||||
@@ -560,7 +630,6 @@ 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
|
||||||
@@ -573,7 +642,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
.ToArray();
|
.ToArray();
|
||||||
if (skippedModules.Length > 0)
|
if (skippedModules.Length > 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (modulePaths.Length == 0)
|
if (modulePaths.Length == 0)
|
||||||
@@ -581,8 +650,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
|
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||||
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
||||||
var loadedModules = 0;
|
var loadedModules = 0;
|
||||||
var failedModules = 0;
|
var failedModules = 0;
|
||||||
var mergedImportCount = 0;
|
var mergedImportCount = 0;
|
||||||
@@ -617,18 +686,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
||||||
loadedModules++;
|
loadedModules++;
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
$"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++;
|
||||||
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
|
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||||
return loadedImages;
|
return loadedImages;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -648,8 +717,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (rebound != 0 || unresolved != 0)
|
if (rebound != 0 || unresolved != 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -681,8 +750,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Warning(
|
||||||
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -694,8 +763,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Error(
|
||||||
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
unresolved++;
|
unresolved++;
|
||||||
@@ -704,8 +773,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
|
|
||||||
if (logRebind)
|
if (logRebind)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
rebound++;
|
rebound++;
|
||||||
@@ -741,8 +810,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
{
|
{
|
||||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Warning(
|
||||||
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||||
}
|
}
|
||||||
|
|
||||||
continue;
|
continue;
|
||||||
@@ -851,8 +920,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
image.EntryPoint,
|
image.EntryPoint,
|
||||||
isMain,
|
isMain,
|
||||||
isSystemModule);
|
isSystemModule);
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
$"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,6 +14,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<PackageReference Include="Iced" />
|
<PackageReference Include="Iced" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<PropertyGroup>
|
<PropertyGroup>
|
||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|||||||
@@ -72,6 +72,7 @@
|
|||||||
"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, )"
|
||||||
}
|
}
|
||||||
@@ -88,6 +89,16 @@
|
|||||||
"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, )",
|
||||||
|
|||||||
@@ -0,0 +1,198 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
@@ -0,0 +1,26 @@
|
|||||||
|
// 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();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
#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");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,373 @@
|
|||||||
|
#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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,141 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,92 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,49 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,63 @@
|
|||||||
|
#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; }
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
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.
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
#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]];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,172 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,34 @@
|
|||||||
|
#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];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,426 @@
|
|||||||
|
#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);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,133 @@
|
|||||||
|
#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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,51 @@
|
|||||||
|
#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
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,178 @@
|
|||||||
|
#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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,150 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
// 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,657 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,156 @@
|
|||||||
|
// 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",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
// 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,94 @@
|
|||||||
|
// 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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
// 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);
|
||||||
@@ -0,0 +1,291 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
<!--
|
||||||
|
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>
|
||||||
@@ -0,0 +1,301 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,189 @@
|
|||||||
|
{
|
||||||
|
"version": 2,
|
||||||
|
"dependencies": {
|
||||||
|
"net10.0": {
|
||||||
|
"Avalonia": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia.BuildServices": "11.3.2",
|
||||||
|
"Avalonia.Remote.Protocol": "11.3.18",
|
||||||
|
"MicroCom.Runtime": "0.11.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Desktop": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18",
|
||||||
|
"Avalonia.Native": "11.3.18",
|
||||||
|
"Avalonia.Skia": "11.3.18",
|
||||||
|
"Avalonia.Win32": "11.3.18",
|
||||||
|
"Avalonia.X11": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Fonts.Inter": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Themes.Fluent": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[11.3.18, )",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Tmds.DBus.Protocol": {
|
||||||
|
"type": "Direct",
|
||||||
|
"requested": "[0.21.3, )",
|
||||||
|
"resolved": "0.21.3",
|
||||||
|
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||||
|
},
|
||||||
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.1.25547.20250602",
|
||||||
|
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||||
|
},
|
||||||
|
"Avalonia.BuildServices": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.2",
|
||||||
|
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||||
|
},
|
||||||
|
"Avalonia.FreeDesktop": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18",
|
||||||
|
"Tmds.DBus.Protocol": "0.21.3"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Native": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Remote.Protocol": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
||||||
|
},
|
||||||
|
"Avalonia.Skia": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18",
|
||||||
|
"HarfBuzzSharp": "8.3.1.1",
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
||||||
|
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
||||||
|
"SkiaSharp": "2.88.9",
|
||||||
|
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
||||||
|
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18",
|
||||||
|
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"Avalonia.X11": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "11.3.18",
|
||||||
|
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
||||||
|
"dependencies": {
|
||||||
|
"Avalonia": "11.3.18",
|
||||||
|
"Avalonia.FreeDesktop": "11.3.18",
|
||||||
|
"Avalonia.Skia": "11.3.18"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
||||||
|
"dependencies": {
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
||||||
|
},
|
||||||
|
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "8.3.1.1",
|
||||||
|
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||||
|
},
|
||||||
|
"MicroCom.Runtime": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "0.11.0",
|
||||||
|
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||||
|
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Linux": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||||
|
"dependencies": {
|
||||||
|
"SkiaSharp": "2.88.9"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.macOS": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||||
|
},
|
||||||
|
"SkiaSharp.NativeAssets.Win32": {
|
||||||
|
"type": "Transitive",
|
||||||
|
"resolved": "2.88.9",
|
||||||
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
|
},
|
||||||
|
"sharpemu.logging": {
|
||||||
|
"type": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,25 +2,20 @@
|
|||||||
// 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
|
public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||||
{
|
{
|
||||||
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 CpuContext(ICpuMemory memory, Generation generation)
|
public ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||||
{
|
|
||||||
Memory = memory ?? throw new ArgumentNullException(nameof(memory));
|
|
||||||
TargetGeneration = generation;
|
|
||||||
}
|
|
||||||
|
|
||||||
public ICpuMemory Memory { get; }
|
public Generation TargetGeneration { get; } = generation;
|
||||||
|
|
||||||
public Generation TargetGeneration { get; }
|
|
||||||
|
|
||||||
public ulong Rip { get; set; }
|
public ulong Rip { get; set; }
|
||||||
|
|
||||||
@@ -130,6 +125,91 @@ public sealed class CpuContext
|
|||||||
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)];
|
||||||
@@ -150,6 +230,33 @@ public sealed class CpuContext
|
|||||||
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];
|
||||||
@@ -169,4 +276,23 @@ public sealed class CpuContext
|
|||||||
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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
// 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.Diagnostics;
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
public readonly record struct GuestThreadStartRequest(
|
public readonly record struct GuestThreadStartRequest(
|
||||||
@@ -8,14 +10,37 @@ public readonly record struct GuestThreadStartRequest(
|
|||||||
ulong EntryPoint,
|
ulong EntryPoint,
|
||||||
ulong Argument,
|
ulong Argument,
|
||||||
ulong AttributeAddress,
|
ulong AttributeAddress,
|
||||||
string Name);
|
string Name,
|
||||||
|
int Priority,
|
||||||
|
ulong AffinityMask);
|
||||||
|
|
||||||
|
public readonly record struct GuestThreadSnapshot(
|
||||||
|
ulong ThreadHandle,
|
||||||
|
string Name,
|
||||||
|
string State,
|
||||||
|
long ImportCount,
|
||||||
|
string? LastImportNid,
|
||||||
|
ulong LastReturnRip,
|
||||||
|
string? BlockReason);
|
||||||
|
|
||||||
public interface IGuestThreadScheduler
|
public interface IGuestThreadScheduler
|
||||||
{
|
{
|
||||||
|
bool SupportsGuestContextTransfer { get; }
|
||||||
|
|
||||||
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
||||||
|
|
||||||
|
bool TryJoinThread(
|
||||||
|
CpuContext callerContext,
|
||||||
|
ulong threadHandle,
|
||||||
|
out ulong returnValue,
|
||||||
|
out string? error);
|
||||||
|
|
||||||
void Pump(CpuContext callerContext, string reason);
|
void Pump(CpuContext callerContext, string reason);
|
||||||
|
|
||||||
|
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
|
||||||
|
|
||||||
|
IReadOnlyList<GuestThreadSnapshot> SnapshotThreads();
|
||||||
|
|
||||||
bool TryCallGuestFunction(
|
bool TryCallGuestFunction(
|
||||||
CpuContext callerContext,
|
CpuContext callerContext,
|
||||||
ulong entryPoint,
|
ulong entryPoint,
|
||||||
@@ -71,15 +96,39 @@ public static class GuestThreadExecution
|
|||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static string? _pendingBlockReason;
|
private static string? _pendingBlockReason;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static bool _pendingBlockContinuationValid;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static GuestCpuContinuation _pendingBlockContinuation;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static string? _pendingBlockWakeKey;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static Func<int>? _pendingBlockResumeHandler;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static Func<bool>? _pendingBlockWakeHandler;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static long _pendingBlockDeadlineTimestamp;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static bool _pendingEntryExit;
|
private static bool _pendingEntryExit;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static int _pendingEntryExitStatus;
|
private static ulong _pendingEntryExitValue;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static string? _pendingEntryExitReason;
|
private static string? _pendingEntryExitReason;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static bool _pendingContextTransfer;
|
||||||
|
|
||||||
|
[ThreadStatic]
|
||||||
|
private static GuestCpuContinuation _pendingContextTransferTarget;
|
||||||
|
|
||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static bool _hasCurrentImportCallFrame;
|
private static bool _hasCurrentImportCallFrame;
|
||||||
|
|
||||||
@@ -105,9 +154,17 @@ public static class GuestThreadExecution
|
|||||||
var previous = _currentGuestThreadHandle;
|
var previous = _currentGuestThreadHandle;
|
||||||
_currentGuestThreadHandle = threadHandle;
|
_currentGuestThreadHandle = threadHandle;
|
||||||
_pendingBlockReason = null;
|
_pendingBlockReason = null;
|
||||||
|
_pendingBlockContinuationValid = false;
|
||||||
|
_pendingBlockContinuation = default;
|
||||||
|
_pendingBlockWakeKey = null;
|
||||||
|
_pendingBlockResumeHandler = null;
|
||||||
|
_pendingBlockWakeHandler = null;
|
||||||
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitStatus = 0;
|
_pendingEntryExitValue = 0;
|
||||||
_pendingEntryExitReason = null;
|
_pendingEntryExitReason = null;
|
||||||
|
_pendingContextTransfer = false;
|
||||||
|
_pendingContextTransferTarget = default;
|
||||||
_hasCurrentImportCallFrame = false;
|
_hasCurrentImportCallFrame = false;
|
||||||
_currentImportReturnRip = 0;
|
_currentImportReturnRip = 0;
|
||||||
_currentImportResumeRsp = 0;
|
_currentImportResumeRsp = 0;
|
||||||
@@ -119,9 +176,17 @@ public static class GuestThreadExecution
|
|||||||
{
|
{
|
||||||
_currentGuestThreadHandle = previousThreadHandle;
|
_currentGuestThreadHandle = previousThreadHandle;
|
||||||
_pendingBlockReason = null;
|
_pendingBlockReason = null;
|
||||||
|
_pendingBlockContinuationValid = false;
|
||||||
|
_pendingBlockContinuation = default;
|
||||||
|
_pendingBlockWakeKey = null;
|
||||||
|
_pendingBlockResumeHandler = null;
|
||||||
|
_pendingBlockWakeHandler = null;
|
||||||
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitStatus = 0;
|
_pendingEntryExitValue = 0;
|
||||||
_pendingEntryExitReason = null;
|
_pendingEntryExitReason = null;
|
||||||
|
_pendingContextTransfer = false;
|
||||||
|
_pendingContextTransferTarget = default;
|
||||||
_hasCurrentImportCallFrame = false;
|
_hasCurrentImportCallFrame = false;
|
||||||
_currentImportReturnRip = 0;
|
_currentImportReturnRip = 0;
|
||||||
_currentImportResumeRsp = 0;
|
_currentImportResumeRsp = 0;
|
||||||
@@ -140,7 +205,15 @@ public static class GuestThreadExecution
|
|||||||
_currentFiberAddress = previousFiberAddress;
|
_currentFiberAddress = previousFiberAddress;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool RequestCurrentThreadBlock(string reason)
|
public static bool RequestCurrentThreadBlock(string reason) => RequestCurrentThreadBlock(null, reason);
|
||||||
|
|
||||||
|
public static bool RequestCurrentThreadBlock(
|
||||||
|
CpuContext? context,
|
||||||
|
string reason,
|
||||||
|
string? wakeKey = null,
|
||||||
|
Func<int>? resumeHandler = null,
|
||||||
|
Func<bool>? wakeHandler = null,
|
||||||
|
long blockDeadlineTimestamp = 0)
|
||||||
{
|
{
|
||||||
if (!IsGuestThread)
|
if (!IsGuestThread)
|
||||||
{
|
{
|
||||||
@@ -148,31 +221,167 @@ public static class GuestThreadExecution
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
_pendingBlockReason = string.IsNullOrWhiteSpace(reason) ? "guest_thread_blocked" : reason;
|
||||||
|
_pendingBlockWakeKey = string.IsNullOrWhiteSpace(wakeKey) ? _pendingBlockReason : wakeKey;
|
||||||
|
_pendingBlockResumeHandler = resumeHandler;
|
||||||
|
_pendingBlockWakeHandler = wakeHandler;
|
||||||
|
_pendingBlockDeadlineTimestamp = blockDeadlineTimestamp;
|
||||||
|
if (context is not null && TryCaptureCurrentBlockContinuation(context, out var continuation))
|
||||||
|
{
|
||||||
|
_pendingBlockContinuation = continuation;
|
||||||
|
_pendingBlockContinuationValid = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_pendingBlockContinuation = default;
|
||||||
|
_pendingBlockContinuationValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryConsumeCurrentThreadBlock(out string reason)
|
public static bool TryConsumeCurrentThreadBlock(out string reason)
|
||||||
|
{
|
||||||
|
return TryConsumeCurrentThreadBlock(out reason, out _, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryConsumeCurrentThreadBlock(
|
||||||
|
out string reason,
|
||||||
|
out GuestCpuContinuation continuation,
|
||||||
|
out bool hasContinuation)
|
||||||
|
{
|
||||||
|
return TryConsumeCurrentThreadBlock(
|
||||||
|
out reason,
|
||||||
|
out continuation,
|
||||||
|
out hasContinuation,
|
||||||
|
out _,
|
||||||
|
out _,
|
||||||
|
out _,
|
||||||
|
out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryConsumeCurrentThreadBlock(
|
||||||
|
out string reason,
|
||||||
|
out GuestCpuContinuation continuation,
|
||||||
|
out bool hasContinuation,
|
||||||
|
out string wakeKey,
|
||||||
|
out Func<int>? resumeHandler,
|
||||||
|
out Func<bool>? wakeHandler)
|
||||||
|
{
|
||||||
|
return TryConsumeCurrentThreadBlock(
|
||||||
|
out reason,
|
||||||
|
out continuation,
|
||||||
|
out hasContinuation,
|
||||||
|
out wakeKey,
|
||||||
|
out resumeHandler,
|
||||||
|
out wakeHandler,
|
||||||
|
out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryConsumeCurrentThreadBlock(
|
||||||
|
out string reason,
|
||||||
|
out GuestCpuContinuation continuation,
|
||||||
|
out bool hasContinuation,
|
||||||
|
out string wakeKey,
|
||||||
|
out Func<int>? resumeHandler,
|
||||||
|
out Func<bool>? wakeHandler,
|
||||||
|
out long blockDeadlineTimestamp)
|
||||||
{
|
{
|
||||||
reason = _pendingBlockReason ?? string.Empty;
|
reason = _pendingBlockReason ?? string.Empty;
|
||||||
if (string.IsNullOrEmpty(reason))
|
if (string.IsNullOrEmpty(reason))
|
||||||
{
|
{
|
||||||
|
continuation = default;
|
||||||
|
hasContinuation = false;
|
||||||
|
wakeKey = string.Empty;
|
||||||
|
resumeHandler = null;
|
||||||
|
wakeHandler = null;
|
||||||
|
blockDeadlineTimestamp = 0;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
continuation = _pendingBlockContinuation;
|
||||||
|
hasContinuation = _pendingBlockContinuationValid;
|
||||||
|
wakeKey = _pendingBlockWakeKey ?? reason;
|
||||||
|
resumeHandler = _pendingBlockResumeHandler;
|
||||||
|
wakeHandler = _pendingBlockWakeHandler;
|
||||||
|
blockDeadlineTimestamp = _pendingBlockDeadlineTimestamp;
|
||||||
_pendingBlockReason = null;
|
_pendingBlockReason = null;
|
||||||
|
_pendingBlockContinuation = default;
|
||||||
|
_pendingBlockContinuationValid = false;
|
||||||
|
_pendingBlockWakeKey = null;
|
||||||
|
_pendingBlockResumeHandler = null;
|
||||||
|
_pendingBlockWakeHandler = null;
|
||||||
|
_pendingBlockDeadlineTimestamp = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static long ComputeDeadlineTimestamp(TimeSpan timeout)
|
||||||
|
{
|
||||||
|
if (timeout <= TimeSpan.Zero)
|
||||||
|
{
|
||||||
|
return Stopwatch.GetTimestamp();
|
||||||
|
}
|
||||||
|
|
||||||
|
var ticks = timeout.TotalSeconds >= long.MaxValue / (double)Stopwatch.Frequency
|
||||||
|
? long.MaxValue
|
||||||
|
: (long)Math.Ceiling(timeout.TotalSeconds * Stopwatch.Frequency);
|
||||||
|
var now = Stopwatch.GetTimestamp();
|
||||||
|
if (long.MaxValue - now <= ticks)
|
||||||
|
{
|
||||||
|
return long.MaxValue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return now + Math.Max(1, ticks);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCaptureCurrentBlockContinuation(CpuContext context, out GuestCpuContinuation continuation)
|
||||||
|
{
|
||||||
|
if (!TryGetCurrentImportCallFrame(out var frame) ||
|
||||||
|
frame.ReturnRip < 65536 ||
|
||||||
|
frame.ResumeRsp == 0 ||
|
||||||
|
frame.ReturnSlotAddress == 0)
|
||||||
|
{
|
||||||
|
continuation = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation = new GuestCpuContinuation(
|
||||||
|
frame.ReturnRip,
|
||||||
|
frame.ResumeRsp,
|
||||||
|
frame.ReturnSlotAddress,
|
||||||
|
context.Rflags,
|
||||||
|
context.FsBase,
|
||||||
|
context.GsBase,
|
||||||
|
0,
|
||||||
|
context[CpuRegister.Rcx],
|
||||||
|
context[CpuRegister.Rdx],
|
||||||
|
context[CpuRegister.Rbx],
|
||||||
|
context[CpuRegister.Rbp],
|
||||||
|
context[CpuRegister.Rsi],
|
||||||
|
context[CpuRegister.Rdi],
|
||||||
|
context[CpuRegister.R8],
|
||||||
|
context[CpuRegister.R9],
|
||||||
|
context[CpuRegister.R12],
|
||||||
|
context[CpuRegister.R13],
|
||||||
|
context[CpuRegister.R14],
|
||||||
|
context[CpuRegister.R15]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static void RequestCurrentEntryExit(string reason, int status)
|
public static void RequestCurrentEntryExit(string reason, int status)
|
||||||
|
{
|
||||||
|
RequestCurrentEntryExit(reason, unchecked((ulong)(long)status));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void RequestCurrentEntryExit(string reason, ulong value)
|
||||||
{
|
{
|
||||||
_pendingEntryExit = true;
|
_pendingEntryExit = true;
|
||||||
_pendingEntryExitStatus = status;
|
_pendingEntryExitValue = value;
|
||||||
_pendingEntryExitReason = string.IsNullOrWhiteSpace(reason) ? "guest_entry_exit" : reason;
|
_pendingEntryExitReason = string.IsNullOrWhiteSpace(reason) ? "guest_entry_exit" : reason;
|
||||||
}
|
}
|
||||||
|
|
||||||
public static bool TryConsumeCurrentEntryExit(out int status, out string reason)
|
public static bool TryConsumeCurrentEntryExit(out ulong value, out string reason)
|
||||||
{
|
{
|
||||||
status = _pendingEntryExitStatus;
|
value = _pendingEntryExitValue;
|
||||||
reason = _pendingEntryExitReason ?? string.Empty;
|
reason = _pendingEntryExitReason ?? string.Empty;
|
||||||
if (!_pendingEntryExit)
|
if (!_pendingEntryExit)
|
||||||
{
|
{
|
||||||
@@ -180,11 +389,30 @@ public static class GuestThreadExecution
|
|||||||
}
|
}
|
||||||
|
|
||||||
_pendingEntryExit = false;
|
_pendingEntryExit = false;
|
||||||
_pendingEntryExitStatus = 0;
|
_pendingEntryExitValue = 0;
|
||||||
_pendingEntryExitReason = null;
|
_pendingEntryExitReason = null;
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static void RequestCurrentContextTransfer(GuestCpuContinuation target)
|
||||||
|
{
|
||||||
|
_pendingContextTransferTarget = target;
|
||||||
|
_pendingContextTransfer = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool TryConsumeCurrentContextTransfer(out GuestCpuContinuation target)
|
||||||
|
{
|
||||||
|
target = _pendingContextTransferTarget;
|
||||||
|
if (!_pendingContextTransfer)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_pendingContextTransfer = false;
|
||||||
|
_pendingContextTransferTarget = default;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public static GuestImportCallFrame EnterImportCallFrame(
|
public static GuestImportCallFrame EnterImportCallFrame(
|
||||||
ulong returnRip,
|
ulong returnRip,
|
||||||
ulong resumeRsp,
|
ulong resumeRsp,
|
||||||
|
|||||||
@@ -47,6 +47,7 @@ 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))
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -105,6 +106,7 @@ 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))
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine($"[HLE] 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;
|
||||||
@@ -112,11 +114,13 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
|
|
||||||
if ((export.Target & context.TargetGeneration) == 0)
|
if ((export.Target & context.TargetGeneration) == 0)
|
||||||
{
|
{
|
||||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||||
|
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,6 +40,13 @@ 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>
|
||||||
@@ -50,6 +57,13 @@ 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,6 +8,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<PropertyGroup>
|
||||||
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
|
</PropertyGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -0,0 +1,313 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
|
internal enum Gen5ShaderEncoding
|
||||||
|
{
|
||||||
|
Sop1,
|
||||||
|
Sop2,
|
||||||
|
Sopc,
|
||||||
|
Sopp,
|
||||||
|
Sopk,
|
||||||
|
Smrd,
|
||||||
|
Smem,
|
||||||
|
Mubuf,
|
||||||
|
Mtbuf,
|
||||||
|
Vop1,
|
||||||
|
Vop2,
|
||||||
|
Vopc,
|
||||||
|
Vop3,
|
||||||
|
Vintrp,
|
||||||
|
Ds,
|
||||||
|
Flat,
|
||||||
|
Vop3p,
|
||||||
|
Mimg,
|
||||||
|
Exp,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum Gen5OperandKind
|
||||||
|
{
|
||||||
|
ScalarRegister,
|
||||||
|
VectorRegister,
|
||||||
|
EncodedConstant,
|
||||||
|
LiteralConstant,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum Gen5ShaderResourceKind
|
||||||
|
{
|
||||||
|
ReadOnlyTexture,
|
||||||
|
ReadWriteTexture,
|
||||||
|
Sampler,
|
||||||
|
ConstantBuffer,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum Gen5PixelOutputKind
|
||||||
|
{
|
||||||
|
Float,
|
||||||
|
Uint,
|
||||||
|
Sint,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum Gen5SpirvStage
|
||||||
|
{
|
||||||
|
Vertex,
|
||||||
|
Pixel,
|
||||||
|
Compute,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record Gen5SpirvShader(
|
||||||
|
byte[] Spirv,
|
||||||
|
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||||
|
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
||||||
|
uint AttributeCount,
|
||||||
|
IReadOnlyList<Gen5VertexInputBinding> VertexInputs);
|
||||||
|
|
||||||
|
internal readonly record struct Gen5ShaderResourceMapping(
|
||||||
|
Gen5ShaderResourceKind Kind,
|
||||||
|
uint Slot,
|
||||||
|
uint OffsetDwords,
|
||||||
|
bool SizeFlag);
|
||||||
|
|
||||||
|
internal sealed record Gen5ShaderMetadata(
|
||||||
|
uint ExtendedUserDataSizeDwords,
|
||||||
|
uint ShaderResourceTableSizeDwords,
|
||||||
|
IReadOnlyDictionary<uint, uint> DirectResources,
|
||||||
|
IReadOnlyList<Gen5ShaderResourceMapping> Resources);
|
||||||
|
|
||||||
|
internal readonly record struct Gen5ComputeSystemRegisters(
|
||||||
|
uint? WorkGroupXRegister,
|
||||||
|
uint? WorkGroupYRegister,
|
||||||
|
uint? WorkGroupZRegister,
|
||||||
|
uint? ThreadGroupSizeRegister)
|
||||||
|
{
|
||||||
|
public bool TryGetExpression(uint scalarRegister, out string expression)
|
||||||
|
{
|
||||||
|
if (WorkGroupXRegister == scalarRegister)
|
||||||
|
{
|
||||||
|
expression = "gl_WorkGroupID.x";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WorkGroupYRegister == scalarRegister)
|
||||||
|
{
|
||||||
|
expression = "gl_WorkGroupID.y";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (WorkGroupZRegister == scalarRegister)
|
||||||
|
{
|
||||||
|
expression = "gl_WorkGroupID.z";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ThreadGroupSizeRegister == scalarRegister)
|
||||||
|
{
|
||||||
|
expression = "(gl_WorkGroupSize.x * gl_WorkGroupSize.y * gl_WorkGroupSize.z)";
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
expression = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void ClearStaticValues(Span<uint> scalarRegisters)
|
||||||
|
{
|
||||||
|
ClearStaticValue(scalarRegisters, WorkGroupXRegister);
|
||||||
|
ClearStaticValue(scalarRegisters, WorkGroupYRegister);
|
||||||
|
ClearStaticValue(scalarRegisters, WorkGroupZRegister);
|
||||||
|
ClearStaticValue(scalarRegisters, ThreadGroupSizeRegister);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ClearStaticValue(Span<uint> scalarRegisters, uint? scalarRegister)
|
||||||
|
{
|
||||||
|
if (scalarRegister is { } register && register < scalarRegisters.Length)
|
||||||
|
{
|
||||||
|
scalarRegisters[(int)register] = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record Gen5ShaderState(
|
||||||
|
Gen5ShaderProgram Program,
|
||||||
|
IReadOnlyList<uint> UserData,
|
||||||
|
Gen5ShaderMetadata? Metadata,
|
||||||
|
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
|
||||||
|
uint UserDataScalarRegisterBase = 0);
|
||||||
|
|
||||||
|
internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
|
||||||
|
{
|
||||||
|
public static Gen5Operand Scalar(uint index) =>
|
||||||
|
new(Gen5OperandKind.ScalarRegister, index);
|
||||||
|
|
||||||
|
public static Gen5Operand Vector(uint index) =>
|
||||||
|
new(Gen5OperandKind.VectorRegister, index);
|
||||||
|
|
||||||
|
public static Gen5Operand Source(uint encoded, uint? literal = null)
|
||||||
|
{
|
||||||
|
if (encoded >= 256)
|
||||||
|
{
|
||||||
|
return Vector(encoded - 256);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoded is 249 or 255 && literal.HasValue)
|
||||||
|
{
|
||||||
|
return new(Gen5OperandKind.LiteralConstant, literal.Value);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (encoded <= 105 || encoded is 106 or 107 or 124 or 126 or 127)
|
||||||
|
{
|
||||||
|
return Scalar(encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
return new(Gen5OperandKind.EncodedConstant, encoded);
|
||||||
|
}
|
||||||
|
|
||||||
|
public override string ToString() => Kind switch
|
||||||
|
{
|
||||||
|
Gen5OperandKind.ScalarRegister => $"s{Value}",
|
||||||
|
Gen5OperandKind.VectorRegister => $"v{Value}",
|
||||||
|
Gen5OperandKind.LiteralConstant => $"0x{Value:X8}",
|
||||||
|
_ => $"src[{Value}]",
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
internal abstract record Gen5InstructionControl;
|
||||||
|
|
||||||
|
internal sealed record Gen5ImageControl(
|
||||||
|
uint Dmask,
|
||||||
|
uint VectorAddress,
|
||||||
|
IReadOnlyList<uint> AddressRegisters,
|
||||||
|
uint VectorData,
|
||||||
|
uint ScalarResource,
|
||||||
|
uint ScalarSampler,
|
||||||
|
uint Dimension,
|
||||||
|
bool IsArray,
|
||||||
|
bool Glc,
|
||||||
|
bool Slc) : Gen5InstructionControl
|
||||||
|
{
|
||||||
|
public uint GetAddressRegister(int component) =>
|
||||||
|
component < AddressRegisters.Count
|
||||||
|
? AddressRegisters[component]
|
||||||
|
: VectorAddress + (uint)component;
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed record Gen5GlobalMemoryControl(
|
||||||
|
uint DwordCount,
|
||||||
|
uint VectorAddress,
|
||||||
|
uint VectorData,
|
||||||
|
uint ScalarAddress,
|
||||||
|
int OffsetBytes,
|
||||||
|
bool Glc,
|
||||||
|
bool Slc) : Gen5InstructionControl;
|
||||||
|
|
||||||
|
internal sealed record Gen5BufferMemoryControl(
|
||||||
|
uint DwordCount,
|
||||||
|
uint VectorAddress,
|
||||||
|
uint VectorData,
|
||||||
|
uint ScalarResource,
|
||||||
|
int OffsetBytes,
|
||||||
|
bool IndexEnabled,
|
||||||
|
bool OffsetEnabled,
|
||||||
|
bool Glc,
|
||||||
|
bool Slc) : Gen5InstructionControl;
|
||||||
|
|
||||||
|
internal sealed record Gen5ExportControl(
|
||||||
|
uint Target,
|
||||||
|
uint EnableMask,
|
||||||
|
bool Compressed,
|
||||||
|
bool Done,
|
||||||
|
bool ValidMask) : Gen5InstructionControl;
|
||||||
|
|
||||||
|
internal sealed record Gen5InterpolationControl(
|
||||||
|
uint Attribute,
|
||||||
|
uint Channel) : Gen5InstructionControl;
|
||||||
|
|
||||||
|
internal sealed record Gen5Vop3Control(
|
||||||
|
uint AbsoluteMask,
|
||||||
|
uint NegateMask,
|
||||||
|
uint OutputModifier,
|
||||||
|
bool Clamp,
|
||||||
|
uint? ScalarDestination) : Gen5InstructionControl;
|
||||||
|
|
||||||
|
internal sealed record Gen5SdwaControl(
|
||||||
|
uint DestinationSelect,
|
||||||
|
uint Source0Select,
|
||||||
|
uint Source1Select,
|
||||||
|
uint AbsoluteMask,
|
||||||
|
uint NegateMask,
|
||||||
|
uint OutputModifier,
|
||||||
|
bool Clamp) : Gen5InstructionControl;
|
||||||
|
|
||||||
|
internal sealed record Gen5DppControl(
|
||||||
|
uint Control,
|
||||||
|
bool FetchInactive,
|
||||||
|
bool BoundControl,
|
||||||
|
uint AbsoluteMask,
|
||||||
|
uint NegateMask,
|
||||||
|
uint BankMask,
|
||||||
|
uint RowMask) : Gen5InstructionControl;
|
||||||
|
|
||||||
|
internal sealed record Gen5ScalarMemoryControl(
|
||||||
|
uint DestinationCount,
|
||||||
|
int ImmediateOffsetBytes,
|
||||||
|
uint? DynamicOffsetRegister) : Gen5InstructionControl;
|
||||||
|
|
||||||
|
internal sealed record Gen5DataShareControl(
|
||||||
|
uint Offset0,
|
||||||
|
uint Offset1,
|
||||||
|
bool Gds) : Gen5InstructionControl;
|
||||||
|
|
||||||
|
internal sealed record Gen5ImageBinding(
|
||||||
|
uint Pc,
|
||||||
|
string Opcode,
|
||||||
|
Gen5ImageControl Control,
|
||||||
|
IReadOnlyList<uint> ResourceDescriptor,
|
||||||
|
IReadOnlyList<uint> SamplerDescriptor,
|
||||||
|
uint? MipLevel);
|
||||||
|
|
||||||
|
internal sealed record Gen5GlobalMemoryBinding(
|
||||||
|
uint ScalarAddress,
|
||||||
|
ulong BaseAddress,
|
||||||
|
IReadOnlyList<uint> InstructionPcs,
|
||||||
|
byte[] Data);
|
||||||
|
|
||||||
|
internal sealed record Gen5VertexInputBinding(
|
||||||
|
uint Pc,
|
||||||
|
uint Location,
|
||||||
|
uint ComponentCount,
|
||||||
|
uint DataFormat,
|
||||||
|
uint NumberFormat,
|
||||||
|
ulong BaseAddress,
|
||||||
|
uint Stride,
|
||||||
|
uint OffsetBytes,
|
||||||
|
byte[] Data);
|
||||||
|
|
||||||
|
internal sealed record Gen5ShaderEvaluation(
|
||||||
|
IReadOnlyList<uint> InitialScalarRegisters,
|
||||||
|
IReadOnlyList<uint> ScalarRegisters,
|
||||||
|
IReadOnlyDictionary<uint, IReadOnlyList<uint>> ScalarRegistersByPc,
|
||||||
|
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
||||||
|
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||||
|
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
|
||||||
|
IReadOnlySet<uint>? RuntimeScalarRegisters = null,
|
||||||
|
IReadOnlyList<Gen5VertexInputBinding>? VertexInputs = null);
|
||||||
|
|
||||||
|
internal sealed record Gen5ShaderInstruction(
|
||||||
|
uint Pc,
|
||||||
|
Gen5ShaderEncoding Encoding,
|
||||||
|
string Opcode,
|
||||||
|
IReadOnlyList<uint> Words,
|
||||||
|
IReadOnlyList<Gen5Operand> Sources,
|
||||||
|
IReadOnlyList<Gen5Operand> Destinations,
|
||||||
|
Gen5InstructionControl? Control);
|
||||||
|
|
||||||
|
internal sealed record Gen5ShaderProgram(
|
||||||
|
ulong Address,
|
||||||
|
IReadOnlyList<Gen5ShaderInstruction> Instructions)
|
||||||
|
{
|
||||||
|
public IEnumerable<Gen5ImageControl> ImageResources =>
|
||||||
|
Instructions
|
||||||
|
.Select(instruction => instruction.Control)
|
||||||
|
.OfType<Gen5ImageControl>();
|
||||||
|
}
|
||||||
@@ -0,0 +1,124 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
|
internal static class Gen5ShaderMetadataReader
|
||||||
|
{
|
||||||
|
private const ulong ShaderUserDataOffset = 0x08;
|
||||||
|
private const int ResourceClassCount = 4;
|
||||||
|
private const int MaxMetadataEntries = 4096;
|
||||||
|
|
||||||
|
public static bool TryRead(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong shaderHeaderAddress,
|
||||||
|
out Gen5ShaderMetadata metadata)
|
||||||
|
{
|
||||||
|
metadata = default!;
|
||||||
|
if (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
||||||
|
userDataAddress == 0 ||
|
||||||
|
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resourceOffsets = new ulong[ResourceClassCount];
|
||||||
|
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||||
|
{
|
||||||
|
if (!ctx.TryReadUInt64(
|
||||||
|
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
|
||||||
|
out resourceOffsets[resourceClass]))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt16(userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||||
|
!ctx.TryReadUInt16(userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||||
|
!ctx.TryReadUInt16(userDataAddress + 0x2C, out var directResourceCount) ||
|
||||||
|
directResourceCount > MaxMetadataEntries)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var resourceCounts = new ushort[ResourceClassCount];
|
||||||
|
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||||
|
{
|
||||||
|
if (!ctx.TryReadUInt16(
|
||||||
|
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
|
||||||
|
out resourceCounts[resourceClass]) ||
|
||||||
|
resourceCounts[resourceClass] > MaxMetadataEntries)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var directResources = new Dictionary<uint, uint>();
|
||||||
|
if (directResourceCount != 0)
|
||||||
|
{
|
||||||
|
if (directResourceOffsetsAddress == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint type = 0; type < directResourceCount; type++)
|
||||||
|
{
|
||||||
|
if (!ctx.TryReadUInt16(directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (offset != ushort.MaxValue)
|
||||||
|
{
|
||||||
|
directResources[type] = offset;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var resources = new List<Gen5ShaderResourceMapping>();
|
||||||
|
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||||
|
{
|
||||||
|
var count = resourceCounts[resourceClass];
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resourceOffsets[resourceClass] == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (uint slot = 0; slot < count; slot++)
|
||||||
|
{
|
||||||
|
if (!ctx.TryReadUInt16(
|
||||||
|
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
||||||
|
out var sharp))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var offset = (uint)(sharp & 0x7FFF);
|
||||||
|
if (offset == 0x7FFF)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
resources.Add(new Gen5ShaderResourceMapping(
|
||||||
|
(Gen5ShaderResourceKind)resourceClass,
|
||||||
|
slot,
|
||||||
|
offset,
|
||||||
|
(sharp & 0x8000) != 0));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
metadata = new Gen5ShaderMetadata(
|
||||||
|
extendedUserDataSize,
|
||||||
|
shaderResourceTableSize,
|
||||||
|
directResources,
|
||||||
|
resources);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,102 @@
|
|||||||
|
// 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,167 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
|
internal static class SpirvFixedShaders
|
||||||
|
{
|
||||||
|
public static byte[] CreateFullscreenVertex(uint attributeCount)
|
||||||
|
{
|
||||||
|
var module = new SpirvModuleBuilder();
|
||||||
|
module.AddCapability(SpirvCapability.Shader);
|
||||||
|
|
||||||
|
var voidType = module.TypeVoid();
|
||||||
|
var boolType = module.TypeBool();
|
||||||
|
var uintType = module.TypeInt(32, signed: false);
|
||||||
|
var floatType = module.TypeFloat(32);
|
||||||
|
var vec4Type = module.TypeVector(floatType, 4);
|
||||||
|
var inputUintPointer = module.TypePointer(SpirvStorageClass.Input, uintType);
|
||||||
|
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||||
|
|
||||||
|
var vertexIndex = module.AddGlobalVariable(inputUintPointer, SpirvStorageClass.Input);
|
||||||
|
module.AddName(vertexIndex, "vertexIndex");
|
||||||
|
module.AddDecoration(
|
||||||
|
vertexIndex,
|
||||||
|
SpirvDecoration.BuiltIn,
|
||||||
|
(uint)SpirvBuiltIn.VertexIndex);
|
||||||
|
|
||||||
|
var position = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
|
||||||
|
module.AddName(position, "position");
|
||||||
|
module.AddDecoration(position, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Position);
|
||||||
|
|
||||||
|
var attributes = new uint[attributeCount];
|
||||||
|
for (uint index = 0; index < attributeCount; index++)
|
||||||
|
{
|
||||||
|
attributes[index] =
|
||||||
|
module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
|
||||||
|
module.AddName(attributes[index], $"attr{index}");
|
||||||
|
module.AddDecoration(attributes[index], SpirvDecoration.Location, index);
|
||||||
|
module.AddDecoration(attributes[index], SpirvDecoration.NoPerspective);
|
||||||
|
}
|
||||||
|
|
||||||
|
var functionType = module.TypeFunction(voidType);
|
||||||
|
var main = module.BeginFunction(voidType, functionType);
|
||||||
|
module.AddName(main, "main");
|
||||||
|
module.AddLabel();
|
||||||
|
|
||||||
|
var indexValue = module.AddInstruction(SpirvOp.Load, uintType, vertexIndex);
|
||||||
|
var one = module.Constant(uintType, 1);
|
||||||
|
var two = module.Constant(uintType, 2);
|
||||||
|
var shifted = module.AddInstruction(SpirvOp.ShiftLeftLogical, uintType, indexValue, one);
|
||||||
|
var xBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, shifted, two);
|
||||||
|
var yBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, indexValue, two);
|
||||||
|
var x = module.AddInstruction(SpirvOp.ConvertUToF, floatType, xBits);
|
||||||
|
var y = module.AddInstruction(SpirvOp.ConvertUToF, floatType, yBits);
|
||||||
|
var zero = module.ConstantFloat(floatType, 0f);
|
||||||
|
var oneFloat = module.ConstantFloat(floatType, 1f);
|
||||||
|
var twoFloat = module.ConstantFloat(floatType, 2f);
|
||||||
|
var xPosition = module.AddInstruction(SpirvOp.FMul, floatType, x, twoFloat);
|
||||||
|
xPosition = module.AddInstruction(SpirvOp.FSub, floatType, xPosition, oneFloat);
|
||||||
|
var yPosition = module.AddInstruction(SpirvOp.FMul, floatType, y, twoFloat);
|
||||||
|
yPosition = module.AddInstruction(SpirvOp.FSub, floatType, yPosition, oneFloat);
|
||||||
|
var positionValue = module.AddInstruction(
|
||||||
|
SpirvOp.CompositeConstruct,
|
||||||
|
vec4Type,
|
||||||
|
xPosition,
|
||||||
|
yPosition,
|
||||||
|
zero,
|
||||||
|
oneFloat);
|
||||||
|
module.AddStatement(SpirvOp.Store, position, positionValue);
|
||||||
|
|
||||||
|
var attributeValue = module.AddInstruction(
|
||||||
|
SpirvOp.CompositeConstruct,
|
||||||
|
vec4Type,
|
||||||
|
x,
|
||||||
|
y,
|
||||||
|
zero,
|
||||||
|
oneFloat);
|
||||||
|
foreach (var attribute in attributes)
|
||||||
|
{
|
||||||
|
module.AddStatement(SpirvOp.Store, attribute, attributeValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
module.AddStatement(SpirvOp.Return);
|
||||||
|
module.EndFunction();
|
||||||
|
|
||||||
|
var interfaces = new uint[2 + attributes.Length];
|
||||||
|
interfaces[0] = vertexIndex;
|
||||||
|
interfaces[1] = position;
|
||||||
|
attributes.CopyTo(interfaces, 2);
|
||||||
|
module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main", interfaces);
|
||||||
|
_ = boolType;
|
||||||
|
return module.Build();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static byte[] CreateCopyFragment()
|
||||||
|
{
|
||||||
|
var module = new SpirvModuleBuilder();
|
||||||
|
module.AddCapability(SpirvCapability.Shader);
|
||||||
|
|
||||||
|
var voidType = module.TypeVoid();
|
||||||
|
var floatType = module.TypeFloat(32);
|
||||||
|
var vec2Type = module.TypeVector(floatType, 2);
|
||||||
|
var vec4Type = module.TypeVector(floatType, 4);
|
||||||
|
var inputVec4Pointer = module.TypePointer(SpirvStorageClass.Input, vec4Type);
|
||||||
|
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||||
|
var imageType = module.TypeImage(
|
||||||
|
floatType,
|
||||||
|
SpirvImageDim.Dim2D,
|
||||||
|
depth: false,
|
||||||
|
arrayed: false,
|
||||||
|
multisampled: false,
|
||||||
|
sampled: 1,
|
||||||
|
SpirvImageFormat.Unknown);
|
||||||
|
var sampledImageType = module.TypeSampledImage(imageType);
|
||||||
|
var sampledImagePointer =
|
||||||
|
module.TypePointer(SpirvStorageClass.UniformConstant, sampledImageType);
|
||||||
|
|
||||||
|
var attribute = module.AddGlobalVariable(inputVec4Pointer, SpirvStorageClass.Input);
|
||||||
|
module.AddName(attribute, "attr0");
|
||||||
|
module.AddDecoration(attribute, SpirvDecoration.Location, 0);
|
||||||
|
|
||||||
|
var texture = module.AddGlobalVariable(
|
||||||
|
sampledImagePointer,
|
||||||
|
SpirvStorageClass.UniformConstant);
|
||||||
|
module.AddName(texture, "tex0");
|
||||||
|
module.AddDecoration(texture, SpirvDecoration.DescriptorSet, 0);
|
||||||
|
module.AddDecoration(texture, SpirvDecoration.Binding, 1);
|
||||||
|
|
||||||
|
var output = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
|
||||||
|
module.AddName(output, "outColor");
|
||||||
|
module.AddDecoration(output, SpirvDecoration.Location, 0);
|
||||||
|
|
||||||
|
var functionType = module.TypeFunction(voidType);
|
||||||
|
var main = module.BeginFunction(voidType, functionType);
|
||||||
|
module.AddName(main, "main");
|
||||||
|
module.AddLabel();
|
||||||
|
|
||||||
|
var attributeValue = module.AddInstruction(SpirvOp.Load, vec4Type, attribute);
|
||||||
|
var coordinates = module.AddInstruction(
|
||||||
|
SpirvOp.VectorShuffle,
|
||||||
|
vec2Type,
|
||||||
|
attributeValue,
|
||||||
|
attributeValue,
|
||||||
|
0,
|
||||||
|
1);
|
||||||
|
var sampledImage = module.AddInstruction(SpirvOp.Load, sampledImageType, texture);
|
||||||
|
var lod = module.ConstantFloat(floatType, 0f);
|
||||||
|
var color = module.AddInstruction(
|
||||||
|
SpirvOp.ImageSampleExplicitLod,
|
||||||
|
vec4Type,
|
||||||
|
sampledImage,
|
||||||
|
coordinates,
|
||||||
|
2,
|
||||||
|
lod);
|
||||||
|
module.AddStatement(SpirvOp.Store, output, color);
|
||||||
|
module.AddStatement(SpirvOp.Return);
|
||||||
|
module.EndFunction();
|
||||||
|
|
||||||
|
module.AddEntryPoint(
|
||||||
|
SpirvExecutionModel.Fragment,
|
||||||
|
main,
|
||||||
|
"main",
|
||||||
|
[attribute, texture, output]);
|
||||||
|
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||||
|
return module.Build();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,885 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
using System.Text;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Agc;
|
||||||
|
|
||||||
|
internal enum SpirvOp : ushort
|
||||||
|
{
|
||||||
|
Nop = 0,
|
||||||
|
Name = 5,
|
||||||
|
Extension = 10,
|
||||||
|
ExtInstImport = 11,
|
||||||
|
ExtInst = 12,
|
||||||
|
MemoryModel = 14,
|
||||||
|
EntryPoint = 15,
|
||||||
|
ExecutionMode = 16,
|
||||||
|
Capability = 17,
|
||||||
|
TypeVoid = 19,
|
||||||
|
TypeBool = 20,
|
||||||
|
TypeInt = 21,
|
||||||
|
TypeFloat = 22,
|
||||||
|
TypeVector = 23,
|
||||||
|
TypeImage = 25,
|
||||||
|
TypeSampler = 26,
|
||||||
|
TypeSampledImage = 27,
|
||||||
|
TypeArray = 28,
|
||||||
|
TypeRuntimeArray = 29,
|
||||||
|
TypeStruct = 30,
|
||||||
|
TypePointer = 32,
|
||||||
|
TypeFunction = 33,
|
||||||
|
ConstantTrue = 41,
|
||||||
|
ConstantFalse = 42,
|
||||||
|
Constant = 43,
|
||||||
|
ConstantComposite = 44,
|
||||||
|
ConstantNull = 46,
|
||||||
|
Function = 54,
|
||||||
|
FunctionParameter = 55,
|
||||||
|
FunctionEnd = 56,
|
||||||
|
FunctionCall = 57,
|
||||||
|
Variable = 59,
|
||||||
|
Load = 61,
|
||||||
|
Store = 62,
|
||||||
|
AccessChain = 65,
|
||||||
|
ArrayLength = 68,
|
||||||
|
Decorate = 71,
|
||||||
|
VectorExtractDynamic = 77,
|
||||||
|
VectorInsertDynamic = 78,
|
||||||
|
VectorShuffle = 79,
|
||||||
|
CompositeConstruct = 80,
|
||||||
|
CompositeExtract = 81,
|
||||||
|
CompositeInsert = 82,
|
||||||
|
CopyObject = 83,
|
||||||
|
SampledImage = 86,
|
||||||
|
ImageSampleImplicitLod = 87,
|
||||||
|
ImageSampleExplicitLod = 88,
|
||||||
|
ImageSampleDrefImplicitLod = 89,
|
||||||
|
ImageSampleDrefExplicitLod = 90,
|
||||||
|
ImageFetch = 95,
|
||||||
|
ImageGather = 96,
|
||||||
|
ImageDrefGather = 97,
|
||||||
|
ImageRead = 98,
|
||||||
|
ImageWrite = 99,
|
||||||
|
Image = 100,
|
||||||
|
ImageQuerySizeLod = 103,
|
||||||
|
ImageQuerySize = 104,
|
||||||
|
ImageQueryLod = 105,
|
||||||
|
ImageQueryLevels = 106,
|
||||||
|
ImageQuerySamples = 107,
|
||||||
|
ConvertFToU = 109,
|
||||||
|
ConvertFToS = 110,
|
||||||
|
ConvertSToF = 111,
|
||||||
|
ConvertUToF = 112,
|
||||||
|
UConvert = 113,
|
||||||
|
SConvert = 114,
|
||||||
|
FConvert = 115,
|
||||||
|
Bitcast = 124,
|
||||||
|
SNegate = 126,
|
||||||
|
FNegate = 127,
|
||||||
|
IAdd = 128,
|
||||||
|
FAdd = 129,
|
||||||
|
ISub = 130,
|
||||||
|
FSub = 131,
|
||||||
|
IMul = 132,
|
||||||
|
FMul = 133,
|
||||||
|
UDiv = 134,
|
||||||
|
SDiv = 135,
|
||||||
|
FDiv = 136,
|
||||||
|
UMod = 137,
|
||||||
|
SRem = 138,
|
||||||
|
SMod = 139,
|
||||||
|
FRem = 140,
|
||||||
|
FMod = 141,
|
||||||
|
IAddCarry = 149,
|
||||||
|
ISubBorrow = 150,
|
||||||
|
UMulExtended = 151,
|
||||||
|
SMulExtended = 152,
|
||||||
|
Any = 154,
|
||||||
|
All = 155,
|
||||||
|
IsNan = 156,
|
||||||
|
IsInf = 157,
|
||||||
|
LogicalEqual = 164,
|
||||||
|
LogicalNotEqual = 165,
|
||||||
|
LogicalOr = 166,
|
||||||
|
LogicalAnd = 167,
|
||||||
|
LogicalNot = 168,
|
||||||
|
Select = 169,
|
||||||
|
IEqual = 170,
|
||||||
|
INotEqual = 171,
|
||||||
|
UGreaterThan = 172,
|
||||||
|
SGreaterThan = 173,
|
||||||
|
UGreaterThanEqual = 174,
|
||||||
|
SGreaterThanEqual = 175,
|
||||||
|
ULessThan = 176,
|
||||||
|
SLessThan = 177,
|
||||||
|
ULessThanEqual = 178,
|
||||||
|
SLessThanEqual = 179,
|
||||||
|
FOrdEqual = 180,
|
||||||
|
FUnordEqual = 181,
|
||||||
|
FOrdNotEqual = 182,
|
||||||
|
FUnordNotEqual = 183,
|
||||||
|
FOrdLessThan = 184,
|
||||||
|
FUnordLessThan = 185,
|
||||||
|
FOrdGreaterThan = 186,
|
||||||
|
FUnordGreaterThan = 187,
|
||||||
|
FOrdLessThanEqual = 188,
|
||||||
|
FUnordLessThanEqual = 189,
|
||||||
|
FOrdGreaterThanEqual = 190,
|
||||||
|
FUnordGreaterThanEqual = 191,
|
||||||
|
ShiftRightLogical = 194,
|
||||||
|
ShiftRightArithmetic = 195,
|
||||||
|
ShiftLeftLogical = 196,
|
||||||
|
BitwiseOr = 197,
|
||||||
|
BitwiseXor = 198,
|
||||||
|
BitwiseAnd = 199,
|
||||||
|
Not = 200,
|
||||||
|
BitFieldInsert = 201,
|
||||||
|
BitFieldSExtract = 202,
|
||||||
|
BitFieldUExtract = 203,
|
||||||
|
BitReverse = 204,
|
||||||
|
BitCount = 205,
|
||||||
|
ControlBarrier = 224,
|
||||||
|
MemoryBarrier = 225,
|
||||||
|
AtomicIAdd = 234,
|
||||||
|
Phi = 245,
|
||||||
|
LoopMerge = 246,
|
||||||
|
SelectionMerge = 247,
|
||||||
|
Label = 248,
|
||||||
|
Branch = 249,
|
||||||
|
BranchConditional = 250,
|
||||||
|
Switch = 251,
|
||||||
|
Kill = 252,
|
||||||
|
Return = 253,
|
||||||
|
ReturnValue = 254,
|
||||||
|
Unreachable = 255,
|
||||||
|
GroupNonUniformElect = 333,
|
||||||
|
GroupNonUniformAll = 334,
|
||||||
|
GroupNonUniformAny = 335,
|
||||||
|
GroupNonUniformAllEqual = 336,
|
||||||
|
GroupNonUniformBroadcast = 337,
|
||||||
|
GroupNonUniformBroadcastFirst = 338,
|
||||||
|
GroupNonUniformBallot = 339,
|
||||||
|
GroupNonUniformShuffle = 345,
|
||||||
|
GroupNonUniformShuffleXor = 346,
|
||||||
|
GroupNonUniformShuffleUp = 347,
|
||||||
|
GroupNonUniformShuffleDown = 348,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum SpirvCapability : uint
|
||||||
|
{
|
||||||
|
Shader = 1,
|
||||||
|
Float16 = 9,
|
||||||
|
Float64 = 10,
|
||||||
|
Int64 = 11,
|
||||||
|
Int16 = 22,
|
||||||
|
ImageGatherExtended = 25,
|
||||||
|
StorageImageExtendedFormats = 49,
|
||||||
|
ImageQuery = 50,
|
||||||
|
StorageImageReadWithoutFormat = 55,
|
||||||
|
StorageImageWriteWithoutFormat = 56,
|
||||||
|
GroupNonUniform = 61,
|
||||||
|
GroupNonUniformVote = 62,
|
||||||
|
GroupNonUniformBallot = 64,
|
||||||
|
GroupNonUniformShuffle = 65,
|
||||||
|
RuntimeDescriptorArray = 5302,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum SpirvStorageClass : uint
|
||||||
|
{
|
||||||
|
UniformConstant = 0,
|
||||||
|
Input = 1,
|
||||||
|
Uniform = 2,
|
||||||
|
Output = 3,
|
||||||
|
Workgroup = 4,
|
||||||
|
Private = 6,
|
||||||
|
Function = 7,
|
||||||
|
PushConstant = 9,
|
||||||
|
Image = 11,
|
||||||
|
StorageBuffer = 12,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum SpirvExecutionModel : uint
|
||||||
|
{
|
||||||
|
Vertex = 0,
|
||||||
|
Fragment = 4,
|
||||||
|
GLCompute = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum SpirvExecutionMode : uint
|
||||||
|
{
|
||||||
|
OriginUpperLeft = 7,
|
||||||
|
DepthReplacing = 12,
|
||||||
|
LocalSize = 17,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum SpirvDecoration : uint
|
||||||
|
{
|
||||||
|
Block = 2,
|
||||||
|
ArrayStride = 6,
|
||||||
|
BuiltIn = 11,
|
||||||
|
NoPerspective = 13,
|
||||||
|
Flat = 14,
|
||||||
|
Location = 30,
|
||||||
|
Binding = 33,
|
||||||
|
DescriptorSet = 34,
|
||||||
|
Offset = 35,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum SpirvBuiltIn : uint
|
||||||
|
{
|
||||||
|
Position = 0,
|
||||||
|
VertexIndex = 42,
|
||||||
|
InstanceIndex = 43,
|
||||||
|
FragCoord = 15,
|
||||||
|
FrontFacing = 17,
|
||||||
|
WorkgroupId = 26,
|
||||||
|
LocalInvocationId = 27,
|
||||||
|
GlobalInvocationId = 28,
|
||||||
|
LocalInvocationIndex = 29,
|
||||||
|
SubgroupLocalInvocationId = 41,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum SpirvImageDim : uint
|
||||||
|
{
|
||||||
|
Dim1D = 0,
|
||||||
|
Dim2D = 1,
|
||||||
|
Dim3D = 2,
|
||||||
|
Cube = 3,
|
||||||
|
Buffer = 5,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal enum SpirvImageFormat : uint
|
||||||
|
{
|
||||||
|
Unknown = 0,
|
||||||
|
Rgba32f = 1,
|
||||||
|
Rgba16f = 2,
|
||||||
|
R32f = 3,
|
||||||
|
Rgba8 = 4,
|
||||||
|
Rgba8Snorm = 5,
|
||||||
|
Rg32f = 6,
|
||||||
|
Rg16f = 7,
|
||||||
|
R11fG11fB10f = 8,
|
||||||
|
R16f = 9,
|
||||||
|
Rgba16 = 10,
|
||||||
|
Rgb10A2 = 11,
|
||||||
|
Rg16 = 12,
|
||||||
|
Rg8 = 13,
|
||||||
|
R16 = 14,
|
||||||
|
R8 = 15,
|
||||||
|
Rgba16Snorm = 16,
|
||||||
|
Rg16Snorm = 17,
|
||||||
|
Rg8Snorm = 18,
|
||||||
|
R16Snorm = 19,
|
||||||
|
R8Snorm = 20,
|
||||||
|
Rgba32i = 21,
|
||||||
|
Rgba16i = 22,
|
||||||
|
Rgba8i = 23,
|
||||||
|
R32i = 24,
|
||||||
|
Rg32i = 25,
|
||||||
|
Rg16i = 26,
|
||||||
|
Rg8i = 27,
|
||||||
|
R16i = 28,
|
||||||
|
R8i = 29,
|
||||||
|
Rgba32ui = 30,
|
||||||
|
Rgba16ui = 31,
|
||||||
|
Rgba8ui = 32,
|
||||||
|
R32ui = 33,
|
||||||
|
Rgb10A2ui = 34,
|
||||||
|
Rg32ui = 35,
|
||||||
|
Rg16ui = 36,
|
||||||
|
Rg8ui = 37,
|
||||||
|
R16ui = 38,
|
||||||
|
R8ui = 39,
|
||||||
|
}
|
||||||
|
|
||||||
|
internal sealed class SpirvModuleBuilder
|
||||||
|
{
|
||||||
|
private const uint Magic = 0x07230203;
|
||||||
|
private const uint Version15 = 0x00010500;
|
||||||
|
private const uint Generator = 0x53504500; // "SPE"
|
||||||
|
|
||||||
|
private readonly List<uint> _capabilities = [];
|
||||||
|
private readonly List<uint> _extensions = [];
|
||||||
|
private readonly List<uint> _imports = [];
|
||||||
|
private readonly List<uint> _memoryModel = [];
|
||||||
|
private readonly List<uint> _entryPoints = [];
|
||||||
|
private readonly List<uint> _executionModes = [];
|
||||||
|
private readonly List<uint> _debug = [];
|
||||||
|
private readonly List<uint> _annotations = [];
|
||||||
|
private readonly List<uint> _typesConstantsGlobals = [];
|
||||||
|
private readonly List<uint> _functions = [];
|
||||||
|
private readonly Dictionary<(uint Width, bool Signed), uint> _integerTypes = [];
|
||||||
|
private readonly Dictionary<uint, uint> _floatTypes = [];
|
||||||
|
private readonly Dictionary<(uint Component, uint Count), uint> _vectorTypes = [];
|
||||||
|
private readonly Dictionary<
|
||||||
|
(
|
||||||
|
uint SampledType,
|
||||||
|
SpirvImageDim Dimension,
|
||||||
|
bool Depth,
|
||||||
|
bool Arrayed,
|
||||||
|
bool Multisampled,
|
||||||
|
uint Sampled,
|
||||||
|
SpirvImageFormat Format
|
||||||
|
),
|
||||||
|
uint> _imageTypes = [];
|
||||||
|
private readonly Dictionary<uint, uint> _sampledImageTypes = [];
|
||||||
|
private readonly Dictionary<(SpirvStorageClass Storage, uint Type), uint> _pointerTypes = [];
|
||||||
|
private readonly Dictionary<(uint Element, uint Count), uint> _arrayTypes = [];
|
||||||
|
private readonly Dictionary<uint, uint> _runtimeArrayTypes = [];
|
||||||
|
private readonly Dictionary<string, uint> _functionTypes = [];
|
||||||
|
private readonly Dictionary<(uint Type, ulong Value), uint> _constants = [];
|
||||||
|
private readonly HashSet<SpirvCapability> _declaredCapabilities = [];
|
||||||
|
private readonly Dictionary<string, uint> _extInstImports = [];
|
||||||
|
private uint _nextId = 1;
|
||||||
|
private uint? _voidType;
|
||||||
|
private uint? _boolType;
|
||||||
|
|
||||||
|
public uint AllocateId() => _nextId++;
|
||||||
|
|
||||||
|
public void AddCapability(SpirvCapability capability)
|
||||||
|
{
|
||||||
|
if (_declaredCapabilities.Add(capability))
|
||||||
|
{
|
||||||
|
Emit(_capabilities, SpirvOp.Capability, (uint)capability);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddExtension(string extension) =>
|
||||||
|
EmitWithString(_extensions, SpirvOp.Extension, [], extension);
|
||||||
|
|
||||||
|
public uint ImportExtInst(string name)
|
||||||
|
{
|
||||||
|
if (_extInstImports.TryGetValue(name, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
EmitWithString(_imports, SpirvOp.ExtInstImport, [id], name);
|
||||||
|
_extInstImports.Add(name, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void SetLogicalGlsl450MemoryModel() =>
|
||||||
|
Emit(_memoryModel, SpirvOp.MemoryModel, 0, 1);
|
||||||
|
|
||||||
|
public void AddEntryPoint(
|
||||||
|
SpirvExecutionModel model,
|
||||||
|
uint function,
|
||||||
|
string name,
|
||||||
|
IReadOnlyList<uint> interfaces)
|
||||||
|
{
|
||||||
|
var prefix = new uint[2 + interfaces.Count];
|
||||||
|
prefix[0] = (uint)model;
|
||||||
|
prefix[1] = function;
|
||||||
|
for (var index = 0; index < interfaces.Count; index++)
|
||||||
|
{
|
||||||
|
prefix[index + 2] = interfaces[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
EmitWithString(_entryPoints, SpirvOp.EntryPoint, prefix, name, stringBeforeTailCount: 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddExecutionMode(uint function, SpirvExecutionMode mode, params uint[] operands)
|
||||||
|
{
|
||||||
|
var values = new uint[2 + operands.Length];
|
||||||
|
values[0] = function;
|
||||||
|
values[1] = (uint)mode;
|
||||||
|
operands.CopyTo(values, 2);
|
||||||
|
Emit(_executionModes, SpirvOp.ExecutionMode, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddName(uint target, string name) =>
|
||||||
|
EmitWithString(_debug, SpirvOp.Name, [target], name);
|
||||||
|
|
||||||
|
public void AddDecoration(uint target, SpirvDecoration decoration, params uint[] operands)
|
||||||
|
{
|
||||||
|
var values = new uint[2 + operands.Length];
|
||||||
|
values[0] = target;
|
||||||
|
values[1] = (uint)decoration;
|
||||||
|
operands.CopyTo(values, 2);
|
||||||
|
Emit(_annotations, SpirvOp.Decorate, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public void AddMemberDecoration(
|
||||||
|
uint target,
|
||||||
|
uint member,
|
||||||
|
SpirvDecoration decoration,
|
||||||
|
params uint[] operands)
|
||||||
|
{
|
||||||
|
var values = new uint[3 + operands.Length];
|
||||||
|
values[0] = target;
|
||||||
|
values[1] = member;
|
||||||
|
values[2] = (uint)decoration;
|
||||||
|
operands.CopyTo(values, 3);
|
||||||
|
EmitRaw(_annotations, 72, values);
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypeVoid()
|
||||||
|
{
|
||||||
|
if (_voidType is { } existing)
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.TypeVoid, id);
|
||||||
|
_voidType = id;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypeBool()
|
||||||
|
{
|
||||||
|
if (_boolType is { } existing)
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.TypeBool, id);
|
||||||
|
_boolType = id;
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypeInt(uint width, bool signed)
|
||||||
|
{
|
||||||
|
var key = (width, signed);
|
||||||
|
if (_integerTypes.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.TypeInt, id, width, signed ? 1u : 0u);
|
||||||
|
_integerTypes.Add(key, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypeFloat(uint width)
|
||||||
|
{
|
||||||
|
if (_floatTypes.TryGetValue(width, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.TypeFloat, id, width);
|
||||||
|
_floatTypes.Add(width, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypeVector(uint componentType, uint count)
|
||||||
|
{
|
||||||
|
var key = (componentType, count);
|
||||||
|
if (_vectorTypes.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.TypeVector, id, componentType, count);
|
||||||
|
_vectorTypes.Add(key, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypeImage(
|
||||||
|
uint sampledType,
|
||||||
|
SpirvImageDim dimension,
|
||||||
|
bool depth,
|
||||||
|
bool arrayed,
|
||||||
|
bool multisampled,
|
||||||
|
uint sampled,
|
||||||
|
SpirvImageFormat format)
|
||||||
|
{
|
||||||
|
var key = (
|
||||||
|
sampledType,
|
||||||
|
dimension,
|
||||||
|
depth,
|
||||||
|
arrayed,
|
||||||
|
multisampled,
|
||||||
|
sampled,
|
||||||
|
format);
|
||||||
|
if (_imageTypes.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(
|
||||||
|
_typesConstantsGlobals,
|
||||||
|
SpirvOp.TypeImage,
|
||||||
|
id,
|
||||||
|
sampledType,
|
||||||
|
(uint)dimension,
|
||||||
|
depth ? 1u : 0u,
|
||||||
|
arrayed ? 1u : 0u,
|
||||||
|
multisampled ? 1u : 0u,
|
||||||
|
sampled,
|
||||||
|
(uint)format);
|
||||||
|
_imageTypes.Add(key, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypeSampledImage(uint imageType)
|
||||||
|
{
|
||||||
|
if (_sampledImageTypes.TryGetValue(imageType, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.TypeSampledImage, id, imageType);
|
||||||
|
_sampledImageTypes.Add(imageType, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypeArray(uint elementType, uint count)
|
||||||
|
{
|
||||||
|
var key = (elementType, count);
|
||||||
|
if (_arrayTypes.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var length = Constant(TypeInt(32, false), count);
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.TypeArray, id, elementType, length);
|
||||||
|
_arrayTypes.Add(key, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypeRuntimeArray(uint elementType)
|
||||||
|
{
|
||||||
|
if (_runtimeArrayTypes.TryGetValue(elementType, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.TypeRuntimeArray, id, elementType);
|
||||||
|
_runtimeArrayTypes.Add(elementType, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypeStruct(params uint[] memberTypes)
|
||||||
|
{
|
||||||
|
var id = AllocateId();
|
||||||
|
var operands = new uint[memberTypes.Length + 1];
|
||||||
|
operands[0] = id;
|
||||||
|
memberTypes.CopyTo(operands, 1);
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.TypeStruct, operands);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypePointer(SpirvStorageClass storageClass, uint type)
|
||||||
|
{
|
||||||
|
var key = (storageClass, type);
|
||||||
|
if (_pointerTypes.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.TypePointer, id, (uint)storageClass, type);
|
||||||
|
_pointerTypes.Add(key, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint TypeFunction(uint returnType, params uint[] parameterTypes)
|
||||||
|
{
|
||||||
|
var key = returnType + ":" + string.Join(',', parameterTypes);
|
||||||
|
if (_functionTypes.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
var operands = new uint[parameterTypes.Length + 2];
|
||||||
|
operands[0] = id;
|
||||||
|
operands[1] = returnType;
|
||||||
|
parameterTypes.CopyTo(operands, 2);
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.TypeFunction, operands);
|
||||||
|
_functionTypes.Add(key, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint ConstantBool(bool value)
|
||||||
|
{
|
||||||
|
var type = TypeBool();
|
||||||
|
var key = (type, value ? 1UL : 0UL);
|
||||||
|
if (_constants.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(
|
||||||
|
_typesConstantsGlobals,
|
||||||
|
value ? SpirvOp.ConstantTrue : SpirvOp.ConstantFalse,
|
||||||
|
type,
|
||||||
|
id);
|
||||||
|
_constants.Add(key, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint Constant(uint type, uint value)
|
||||||
|
{
|
||||||
|
var key = (type, (ulong)value);
|
||||||
|
if (_constants.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.Constant, type, id, value);
|
||||||
|
_constants.Add(key, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint Constant64(uint type, ulong value)
|
||||||
|
{
|
||||||
|
var key = (type, value);
|
||||||
|
if (_constants.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(
|
||||||
|
_typesConstantsGlobals,
|
||||||
|
SpirvOp.Constant,
|
||||||
|
type,
|
||||||
|
id,
|
||||||
|
(uint)value,
|
||||||
|
(uint)(value >> 32));
|
||||||
|
_constants.Add(key, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint ConstantFloat(uint type, float value) =>
|
||||||
|
Constant(type, BitConverter.SingleToUInt32Bits(value));
|
||||||
|
|
||||||
|
public uint ConstantComposite(uint type, params uint[] constituents)
|
||||||
|
{
|
||||||
|
var id = AllocateId();
|
||||||
|
var operands = new uint[constituents.Length + 2];
|
||||||
|
operands[0] = type;
|
||||||
|
operands[1] = id;
|
||||||
|
constituents.CopyTo(operands, 2);
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.ConstantComposite, operands);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint ConstantNull(uint type)
|
||||||
|
{
|
||||||
|
var key = (type, ulong.MaxValue);
|
||||||
|
if (_constants.TryGetValue(key, out var existing))
|
||||||
|
{
|
||||||
|
return existing;
|
||||||
|
}
|
||||||
|
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.ConstantNull, type, id);
|
||||||
|
_constants.Add(key, id);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint AddGlobalVariable(
|
||||||
|
uint pointerType,
|
||||||
|
SpirvStorageClass storageClass,
|
||||||
|
uint? initializer = null)
|
||||||
|
{
|
||||||
|
var id = AllocateId();
|
||||||
|
if (initializer.HasValue)
|
||||||
|
{
|
||||||
|
Emit(
|
||||||
|
_typesConstantsGlobals,
|
||||||
|
SpirvOp.Variable,
|
||||||
|
pointerType,
|
||||||
|
id,
|
||||||
|
(uint)storageClass,
|
||||||
|
initializer.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Emit(_typesConstantsGlobals, SpirvOp.Variable, pointerType, id, (uint)storageClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint BeginFunction(uint returnType, uint functionType)
|
||||||
|
{
|
||||||
|
var id = AllocateId();
|
||||||
|
Emit(_functions, SpirvOp.Function, returnType, id, 0, functionType);
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint AddFunctionParameter(uint type) =>
|
||||||
|
EmitResult(_functions, SpirvOp.FunctionParameter, type);
|
||||||
|
|
||||||
|
public uint AddLabel(uint? id = null)
|
||||||
|
{
|
||||||
|
var result = id ?? AllocateId();
|
||||||
|
Emit(_functions, SpirvOp.Label, result);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint AddFunctionVariable(uint pointerType, uint? initializer = null)
|
||||||
|
{
|
||||||
|
var id = AllocateId();
|
||||||
|
if (initializer.HasValue)
|
||||||
|
{
|
||||||
|
Emit(
|
||||||
|
_functions,
|
||||||
|
SpirvOp.Variable,
|
||||||
|
pointerType,
|
||||||
|
id,
|
||||||
|
(uint)SpirvStorageClass.Function,
|
||||||
|
initializer.Value);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Emit(
|
||||||
|
_functions,
|
||||||
|
SpirvOp.Variable,
|
||||||
|
pointerType,
|
||||||
|
id,
|
||||||
|
(uint)SpirvStorageClass.Function);
|
||||||
|
}
|
||||||
|
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public uint AddInstruction(SpirvOp opcode, uint resultType, params uint[] operands) =>
|
||||||
|
EmitResult(_functions, opcode, resultType, operands);
|
||||||
|
|
||||||
|
public void AddStatement(SpirvOp opcode, params uint[] operands) =>
|
||||||
|
Emit(_functions, opcode, operands);
|
||||||
|
|
||||||
|
public void EndFunction() => Emit(_functions, SpirvOp.FunctionEnd);
|
||||||
|
|
||||||
|
public byte[] Build()
|
||||||
|
{
|
||||||
|
if (_memoryModel.Count == 0)
|
||||||
|
{
|
||||||
|
SetLogicalGlsl450MemoryModel();
|
||||||
|
}
|
||||||
|
|
||||||
|
var wordCount =
|
||||||
|
5 +
|
||||||
|
_capabilities.Count +
|
||||||
|
_extensions.Count +
|
||||||
|
_imports.Count +
|
||||||
|
_memoryModel.Count +
|
||||||
|
_entryPoints.Count +
|
||||||
|
_executionModes.Count +
|
||||||
|
_debug.Count +
|
||||||
|
_annotations.Count +
|
||||||
|
_typesConstantsGlobals.Count +
|
||||||
|
_functions.Count;
|
||||||
|
var words = new uint[wordCount];
|
||||||
|
var offset = 0;
|
||||||
|
WriteWord(Magic);
|
||||||
|
WriteWord(Version15);
|
||||||
|
WriteWord(Generator);
|
||||||
|
WriteWord(_nextId);
|
||||||
|
WriteWord(0);
|
||||||
|
WriteSection(_capabilities);
|
||||||
|
WriteSection(_extensions);
|
||||||
|
WriteSection(_imports);
|
||||||
|
WriteSection(_memoryModel);
|
||||||
|
WriteSection(_entryPoints);
|
||||||
|
WriteSection(_executionModes);
|
||||||
|
WriteSection(_debug);
|
||||||
|
WriteSection(_annotations);
|
||||||
|
WriteSection(_typesConstantsGlobals);
|
||||||
|
WriteSection(_functions);
|
||||||
|
var bytes = new byte[wordCount * sizeof(uint)];
|
||||||
|
Buffer.BlockCopy(words, 0, bytes, 0, bytes.Length);
|
||||||
|
return bytes;
|
||||||
|
|
||||||
|
void WriteWord(uint value)
|
||||||
|
{
|
||||||
|
words[offset++] = value;
|
||||||
|
}
|
||||||
|
|
||||||
|
void WriteSection(List<uint> section)
|
||||||
|
{
|
||||||
|
foreach (var value in section)
|
||||||
|
{
|
||||||
|
WriteWord(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private uint EmitResult(
|
||||||
|
List<uint> section,
|
||||||
|
SpirvOp opcode,
|
||||||
|
uint resultType,
|
||||||
|
params uint[] operands)
|
||||||
|
{
|
||||||
|
var result = AllocateId();
|
||||||
|
var values = new uint[operands.Length + 2];
|
||||||
|
values[0] = resultType;
|
||||||
|
values[1] = result;
|
||||||
|
operands.CopyTo(values, 2);
|
||||||
|
Emit(section, opcode, values);
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void Emit(List<uint> section, SpirvOp opcode, params uint[] operands) =>
|
||||||
|
EmitRaw(section, (ushort)opcode, operands);
|
||||||
|
|
||||||
|
private static void EmitRaw(List<uint> section, ushort opcode, params uint[] operands)
|
||||||
|
{
|
||||||
|
section.Add(((uint)(operands.Length + 1) << 16) | opcode);
|
||||||
|
section.AddRange(operands);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void EmitWithString(
|
||||||
|
List<uint> section,
|
||||||
|
SpirvOp opcode,
|
||||||
|
IReadOnlyList<uint> prefix,
|
||||||
|
string value,
|
||||||
|
int stringBeforeTailCount = -1)
|
||||||
|
{
|
||||||
|
var encoded = EncodeString(value);
|
||||||
|
if (stringBeforeTailCount < 0)
|
||||||
|
{
|
||||||
|
var operands = new uint[prefix.Count + encoded.Length];
|
||||||
|
for (var index = 0; index < prefix.Count; index++)
|
||||||
|
{
|
||||||
|
operands[index] = prefix[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded.CopyTo(operands, prefix.Count);
|
||||||
|
Emit(section, opcode, operands);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = new uint[prefix.Count + encoded.Length];
|
||||||
|
for (var index = 0; index < stringBeforeTailCount; index++)
|
||||||
|
{
|
||||||
|
result[index] = prefix[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
encoded.CopyTo(result, stringBeforeTailCount);
|
||||||
|
for (var index = stringBeforeTailCount; index < prefix.Count; index++)
|
||||||
|
{
|
||||||
|
result[index + encoded.Length] = prefix[index];
|
||||||
|
}
|
||||||
|
|
||||||
|
Emit(section, opcode, result);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static uint[] EncodeString(string value)
|
||||||
|
{
|
||||||
|
var byteCount = Encoding.UTF8.GetByteCount(value) + 1;
|
||||||
|
var words = new uint[(byteCount + 3) / 4];
|
||||||
|
Encoding.UTF8.GetBytes(value, System.Runtime.InteropServices.MemoryMarshal.AsBytes(words.AsSpan()));
|
||||||
|
return words;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
using SharpEmu.Libs.Kernel;
|
using SharpEmu.Libs.Kernel;
|
||||||
|
using System.Buffers;
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
|
|
||||||
@@ -23,6 +24,12 @@ 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 =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||||
|
private static readonly bool _traceAmprReads =
|
||||||
|
_traceAmpr ||
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
private sealed class CommandBufferState
|
private sealed class CommandBufferState
|
||||||
{
|
{
|
||||||
@@ -31,6 +38,23 @@ 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",
|
||||||
@@ -75,12 +99,7 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
var buffer = 0UL;
|
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer: 0, size: 0, aux0, aux1, clear: false))
|
||||||
var size = 0UL;
|
|
||||||
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer);
|
|
||||||
_ = ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size);
|
|
||||||
|
|
||||||
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer, size, aux0, aux1, clear: false))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -104,8 +123,9 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, 0) ||
|
Span<byte> auxiliaryPointers = stackalloc byte[sizeof(ulong) * 2];
|
||||||
!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, 0))
|
auxiliaryPointers.Clear();
|
||||||
|
if (!ctx.Memory.TryWrite(commandBuffer + CommandBufferAux0Offset, auxiliaryPointers))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -179,8 +199,15 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out var buffer) ||
|
Span<byte> bufferPointers = stackalloc byte[sizeof(ulong) * 2];
|
||||||
!ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out var size) ||
|
if (!ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, bufferPointers))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers);
|
||||||
|
var size = BinaryPrimitives.ReadUInt64LittleEndian(bufferPointers[sizeof(ulong)..]);
|
||||||
|
if (
|
||||||
!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0))
|
!WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
@@ -240,19 +267,53 @@ 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))
|
||||||
{
|
{
|
||||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
if (destination != 0 && size > 0)
|
||||||
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;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
|
// Offset -1 means "continue after the previous read of this file id".
|
||||||
|
if (fileOffset == unchecked((ulong)(long)-1))
|
||||||
|
{
|
||||||
|
fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size);
|
||||||
|
}
|
||||||
|
else if (fileOffset > long.MaxValue)
|
||||||
|
{
|
||||||
|
fileOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out bytesRead);
|
||||||
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;
|
||||||
@@ -355,24 +416,26 @@ public static class AmprExports
|
|||||||
var commandBuffer = ctx[CpuRegister.Rdi];
|
var commandBuffer = ctx[CpuRegister.Rdi];
|
||||||
var equeue = ctx[CpuRegister.Rsi];
|
var equeue = ctx[CpuRegister.Rsi];
|
||||||
var ident = ctx[CpuRegister.Rdx];
|
var ident = ctx[CpuRegister.Rdx];
|
||||||
var filter = ctx[CpuRegister.Rcx];
|
var completionToken = ctx[CpuRegister.Rcx];
|
||||||
var userData = ctx[CpuRegister.R8];
|
var userData = ctx[CpuRegister.R8];
|
||||||
var data = ctx[CpuRegister.R9];
|
|
||||||
|
|
||||||
if (commandBuffer == 0)
|
if (commandBuffer == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
var extra = 0UL;
|
if (!AppendKernelEventQueueRecord(
|
||||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp] + sizeof(ulong), out extra);
|
ctx,
|
||||||
|
commandBuffer,
|
||||||
if (!AppendKernelEventQueueRecord(ctx, commandBuffer, equeue, ident, filter, userData, data, extra))
|
equeue,
|
||||||
|
ident,
|
||||||
|
completionToken,
|
||||||
|
userData))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceAmpr(ctx, "write_equeue", commandBuffer, equeue, ident);
|
TraceAmpr(ctx, "write_equeue", commandBuffer, ident, completionToken);
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -424,7 +487,7 @@ public static class AmprExports
|
|||||||
var offset = 0UL;
|
var offset = 0UL;
|
||||||
while (offset < writeOffset)
|
while (offset < writeOffset)
|
||||||
{
|
{
|
||||||
if (!TryReadUInt32(ctx, buffer + offset, out var recordType))
|
if (!ctx.TryReadUInt32(buffer + offset, out var recordType))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -472,20 +535,34 @@ public static class AmprExports
|
|||||||
ulong aux1,
|
ulong aux1,
|
||||||
bool clear)
|
bool clear)
|
||||||
{
|
{
|
||||||
|
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
|
||||||
if (clear)
|
if (clear)
|
||||||
{
|
{
|
||||||
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
|
|
||||||
header.Clear();
|
header.Clear();
|
||||||
if (!ctx.Memory.TryWrite(commandBuffer, header))
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!ctx.Memory.TryRead(commandBuffer, header))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
buffer = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferDataOffset..]);
|
||||||
|
size = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferSizeOffset..]);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
|
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSelfOffset..], commandBuffer);
|
||||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, aux0) &&
|
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferDataOffset..], buffer);
|
||||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, aux1) &&
|
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSizeOffset..], size);
|
||||||
WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0);
|
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux0Offset..], aux0);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferAux1Offset..], aux1);
|
||||||
|
if (!ctx.Memory.TryWrite(commandBuffer, header))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset: 0);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
private static bool WriteCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
||||||
@@ -500,6 +577,26 @@ public static class AmprExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
UpdateCommandBufferState(commandBuffer, buffer, size, writeOffset);
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
||||||
|
{
|
||||||
|
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 3];
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(pointers, commandBuffer);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(pointers[sizeof(ulong)..], buffer);
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(pointers[(sizeof(ulong) * 2)..], size);
|
||||||
|
return ctx.Memory.TryWrite(commandBuffer + CommandBufferSelfOffset, pointers);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void UpdateCommandBufferState(
|
||||||
|
ulong commandBuffer,
|
||||||
|
ulong buffer,
|
||||||
|
ulong size,
|
||||||
|
ulong writeOffset)
|
||||||
|
{
|
||||||
var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
var state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
@@ -507,15 +604,6 @@ public static class AmprExports
|
|||||||
state.Size = size;
|
state.Size = size;
|
||||||
state.WriteOffset = writeOffset;
|
state.WriteOffset = writeOffset;
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool WriteVisibleCommandBufferPointers(CpuContext ctx, ulong commandBuffer, ulong buffer, ulong size)
|
|
||||||
{
|
|
||||||
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
|
|
||||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferDataOffset, buffer) &&
|
|
||||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferSizeOffset, size);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryGetCommandBufferState(
|
private static bool TryGetCommandBufferState(
|
||||||
@@ -536,9 +624,11 @@ public static class AmprExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer) &&
|
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 2];
|
||||||
ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size))
|
if (ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, pointers))
|
||||||
{
|
{
|
||||||
|
buffer = BinaryPrimitives.ReadUInt64LittleEndian(pointers);
|
||||||
|
size = BinaryPrimitives.ReadUInt64LittleEndian(pointers[sizeof(ulong)..]);
|
||||||
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
@@ -591,23 +681,48 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
const int ChunkSize = 64 * 1024;
|
const int ChunkSize = 1024 * 1024;
|
||||||
var buffer = new byte[(int)Math.Min((ulong)ChunkSize, size)];
|
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ChunkSize, size));
|
||||||
|
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
if (!TryGetCachedHostFile(hostPath, out var cachedFile, out var openResult))
|
||||||
if (fileOffset >= (ulong)stream.Length)
|
{
|
||||||
|
return openResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
long fileLength;
|
||||||
|
lock (cachedFile.Gate)
|
||||||
|
{
|
||||||
|
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);
|
||||||
var read = stream.Read(buffer, 0, request);
|
int read;
|
||||||
|
lock (cachedFile.Gate)
|
||||||
|
{
|
||||||
|
cachedFile.Stream.Position = unchecked((long)absoluteOffset);
|
||||||
|
read = cachedFile.Stream.Read(buffer, 0, request);
|
||||||
|
}
|
||||||
|
|
||||||
if (read <= 0)
|
if (read <= 0)
|
||||||
{
|
{
|
||||||
break;
|
break;
|
||||||
@@ -629,10 +744,52 @@ public static class AmprExports
|
|||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
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,
|
||||||
@@ -659,20 +816,17 @@ public static class AmprExports
|
|||||||
ulong commandBuffer,
|
ulong commandBuffer,
|
||||||
ulong equeue,
|
ulong equeue,
|
||||||
ulong ident,
|
ulong ident,
|
||||||
ulong filter,
|
ulong completionToken,
|
||||||
ulong userData,
|
ulong userData)
|
||||||
ulong data,
|
|
||||||
ulong extra)
|
|
||||||
{
|
{
|
||||||
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
|
Span<byte> record = stackalloc byte[(int)KernelEventQueueRecordSize];
|
||||||
record.Clear();
|
record.Clear();
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType);
|
BinaryPrimitives.WriteUInt32LittleEndian(record[0x00..], KernelEventQueueRecordType);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(record[0x04..], unchecked((uint)filter));
|
BinaryPrimitives.WriteInt16LittleEndian(record[0x04..], KernelEventQueueCompatExports.KernelEventFilterAmpr);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], equeue);
|
BinaryPrimitives.WriteUInt64LittleEndian(record[0x08..], equeue);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident);
|
BinaryPrimitives.WriteUInt64LittleEndian(record[0x10..], ident);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData);
|
BinaryPrimitives.WriteUInt64LittleEndian(record[0x18..], userData);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], data);
|
BinaryPrimitives.WriteUInt64LittleEndian(record[0x20..], completionToken);
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(record[0x28..], extra);
|
|
||||||
|
|
||||||
return AppendCommandBufferRecord(ctx, commandBuffer, record);
|
return AppendCommandBufferRecord(ctx, commandBuffer, record);
|
||||||
}
|
}
|
||||||
@@ -763,22 +917,9 @@ 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 (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
|
if (!_traceAmpr)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -800,7 +941,7 @@ public static class AmprExports
|
|||||||
string? hostPath,
|
string? hostPath,
|
||||||
int result)
|
int result)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
|
if (!_traceAmprReads)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,156 @@
|
|||||||
|
// 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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -168,12 +168,6 @@ 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))
|
||||||
|
|||||||
@@ -0,0 +1,215 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Audio;
|
||||||
|
|
||||||
|
public static class AudioOut2Exports
|
||||||
|
{
|
||||||
|
// Sized from guest evidence, not SDK headers: Quake keeps its
|
||||||
|
// SceAudioOut2ContextParam on the stack with the frame canary at param+0x60,
|
||||||
|
// and an earlier 0x80-byte ResetParam write zeroed that canary
|
||||||
|
// (__stack_chk_fail right after sceAudioOut2UserCreate, which silently killed
|
||||||
|
// the whole audio init). Stay well below 0x60 and only write the prefix we
|
||||||
|
// populate.
|
||||||
|
private const int AudioOut2ContextParamSize = 0x30;
|
||||||
|
private const int AudioOut2ContextMemorySize = 0x10000;
|
||||||
|
private static long _nextContextHandle = 1;
|
||||||
|
private static long _nextUserHandle = 1;
|
||||||
|
private static int _nextPortId;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "g2tViFIohHE",
|
||||||
|
ExportName = "sceAudioOut2Initialize",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2Initialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "t5YrizufpQc",
|
||||||
|
ExportName = "sceAudioOut2ContextResetParam",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextResetParam(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (paramAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||||
|
param.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x00..], AudioOut2ContextParamSize);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x04..], 2);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x08..], 48000);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(paramAddress, param)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "pDmme7Bgm6E",
|
||||||
|
ExportName = "sceAudioOut2ContextQueryMemory",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (paramAddress == 0 || outMemorySizeAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "0x6o1VVAYSY",
|
||||||
|
ExportName = "sceAudioOut2ContextCreate",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var memoryAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var memorySize = ctx[CpuRegister.Rdx];
|
||||||
|
var outContextAddress = ctx[CpuRegister.Rcx];
|
||||||
|
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||||
|
return ctx.TryWriteUInt64(outContextAddress, handle)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "on6ZH7Abo10",
|
||||||
|
ExportName = "sceAudioOut2ContextDestroy",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "JK2wamZPzwM",
|
||||||
|
ExportName = "sceAudioOut2PortCreate",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2PortCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var type = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var paramAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var outPortAddress = ctx[CpuRegister.Rdx];
|
||||||
|
var contextAddress = ctx[CpuRegister.Rcx];
|
||||||
|
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var portId = unchecked((uint)Interlocked.Increment(ref _nextPortId)) & 0xFF;
|
||||||
|
var handle = 0x2000_0000UL | ((ulong)(uint)type << 16) | portId;
|
||||||
|
return ctx.TryWriteUInt64(outPortAddress, handle)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gatEUKG+Ea4",
|
||||||
|
ExportName = "sceAudioOut2PortGetState",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2PortGetState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var stateAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (handle == 0 || stateAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = (int)((handle >> 16) & 0xFF);
|
||||||
|
Span<byte> state = stackalloc byte[0x20];
|
||||||
|
state.Clear();
|
||||||
|
var output = type == 2 ? 0x40 : 0x01;
|
||||||
|
var channels = type == 2 ? 1 : 2;
|
||||||
|
BinaryPrimitives.WriteUInt16LittleEndian(state[0x00..], unchecked((ushort)output));
|
||||||
|
state[0x02] = unchecked((byte)channels);
|
||||||
|
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(stateAddress, state)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "DImz2Ft9E2g",
|
||||||
|
ExportName = "sceAudioOut2GetSpeakerInfo",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2GetSpeakerInfo(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var infoAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (infoAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> info = stackalloc byte[0x40];
|
||||||
|
info.Clear();
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0x00..], 1);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0x04..], 2);
|
||||||
|
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
||||||
|
|
||||||
|
return ctx.Memory.TryWrite(infoAddress, info)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "cd+Rtw+D1x8",
|
||||||
|
ExportName = "sceAudioOut2PortDestroy",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2PortDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "IaZXJ9M79uo",
|
||||||
|
ExportName = "sceAudioOut2UserDestroy",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "xywYcRB7nbQ",
|
||||||
|
ExportName = "sceAudioOut2UserCreate",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAudioOut2")]
|
||||||
|
public static int AudioOut2UserCreate(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var userId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||||
|
return ctx.TryWriteUInt64(outUserAddress, handle)
|
||||||
|
? ctx.SetReturn(0)
|
||||||
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,234 @@
|
|||||||
|
// 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,302 @@
|
|||||||
|
// 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);
|
||||||
|
}
|
||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// 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,7 +8,6 @@ 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(
|
||||||
@@ -18,11 +17,12 @@ public static class CommonDialogExports
|
|||||||
LibraryName = "libSceCommonDialog")]
|
LibraryName = "libSceCommonDialog")]
|
||||||
public static int CommonDialogInitialize(CpuContext ctx)
|
public static int CommonDialogInitialize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
// Games can initialize the common-dialog service defensively from more than one
|
||||||
? 0
|
// subsystem. Keeping this HLE initialization idempotent avoids turning an error
|
||||||
: AlreadySystemInitialized;
|
// reporting path into a second, unrelated failure.
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
Interlocked.Exchange(ref _initialized, 1);
|
||||||
return result;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|||||||
@@ -0,0 +1,185 @@
|
|||||||
|
// 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 = "",
|
Nid = "9rAeANT2tyE",
|
||||||
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 = "",
|
Nid = "2emaaluWzUw",
|
||||||
ExportName = "__cxa_guard_abort",
|
ExportName = "__cxa_guard_abort",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libc")]
|
LibraryName = "libc")]
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// 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}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -48,15 +48,10 @@ public static class FiberExports
|
|||||||
[ThreadStatic]
|
[ThreadStatic]
|
||||||
private static ulong _currentFiberAddress;
|
private static ulong _currentFiberAddress;
|
||||||
|
|
||||||
[ThreadStatic]
|
private static readonly object _fiberGate = new();
|
||||||
private static bool _fiberReturnRequested;
|
|
||||||
|
|
||||||
[ThreadStatic]
|
|
||||||
private static ulong _fiberReturnArgument;
|
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
|
private static readonly ConcurrentDictionary<ulong, FiberContinuation> _continuations = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, FiberReturnTarget> _returnTargets = new();
|
||||||
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
|
private static readonly ConcurrentDictionary<ulong, FiberStackRange> _stackRanges = new();
|
||||||
private static readonly ConcurrentDictionary<ulong, FiberRunSession> _runSessions = new();
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "hVYD7Ou2pCQ",
|
Nid = "hVYD7Ou2pCQ",
|
||||||
@@ -109,17 +104,17 @@ public static class FiberExports
|
|||||||
var optParam = ctx[CpuRegister.Rdi];
|
var optParam = ctx[CpuRegister.Rdi];
|
||||||
if (optParam == 0)
|
if (optParam == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((optParam & 7) != 0)
|
if ((optParam & 7) != 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorAlignment);
|
return ctx.SetReturn(FiberErrorAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
|
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorInvalid);
|
: ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -132,23 +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 SetReturn(ctx, error);
|
return ctx.SetReturn(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state))
|
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (state != FiberStateIdle)
|
if (state != FiberStateIdle)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorState);
|
return ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
_continuations.TryRemove(fiber, out _);
|
_continuations.TryRemove(fiber, out _);
|
||||||
|
_returnTargets.TryRemove(fiber, out _);
|
||||||
_stackRanges.TryRemove(fiber, out _);
|
_stackRanges.TryRemove(fiber, out _);
|
||||||
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
|
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -231,44 +227,76 @@ public static class FiberExports
|
|||||||
public static int FiberReturnToThread(CpuContext ctx)
|
public static int FiberReturnToThread(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||||
var inferredFiber = false;
|
|
||||||
if (_currentFiberAddress == 0 && fiberAddress != 0)
|
|
||||||
{
|
|
||||||
inferredFiber = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (fiberAddress == 0)
|
if (fiberAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||||
|
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
var returnArgument = ctx[CpuRegister.Rdi];
|
||||||
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
var argOnRunAddress = ctx[CpuRegister.Rsi];
|
||||||
if (argOnRunAddress != 0 && !TryWriteUInt64(ctx, argOnRunAddress, 0))
|
if (argOnRunAddress != 0 && !ctx.TryWriteUInt64(argOnRunAddress, 0))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
_fiberReturnRequested = true;
|
GuestCpuContinuation transferTarget;
|
||||||
_fiberReturnArgument = ctx[CpuRegister.Rdi];
|
ulong previousFiber;
|
||||||
if (_runSessions.TryGetValue(fiberAddress, out var session))
|
lock (_fiberGate)
|
||||||
{
|
|
||||||
session.SetReturn(ctx[CpuRegister.Rdi]);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
|
||||||
{
|
{
|
||||||
_continuations[fiberAddress] = new FiberContinuation(
|
_continuations[fiberAddress] = new FiberContinuation(
|
||||||
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
||||||
argOnRunAddress);
|
argOnRunAddress);
|
||||||
TraceFiber($"yield{(inferredFiber ? "-inferred" : string.Empty)} fiber=0x{fiberAddress:X16} resume=0x{frame.ReturnRip:X16} rsp=0x{frame.ResumeRsp:X16} return_slot=0x{frame.ReturnSlotAddress:X16} arg_out=0x{argOnRunAddress:X16}");
|
|
||||||
}
|
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
||||||
else
|
{
|
||||||
{
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
TraceFiber($"yield-no-frame{(inferredFiber ? "-inferred" : string.Empty)} fiber=0x{fiberAddress:X16} arg_out=0x{argOnRunAddress:X16}");
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
|
}
|
||||||
|
|
||||||
|
previousFiber = returnTarget.PreviousFiber;
|
||||||
|
if (previousFiber != 0)
|
||||||
|
{
|
||||||
|
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
||||||
|
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
||||||
|
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
||||||
|
{
|
||||||
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
|
return ctx.SetReturn(FiberErrorState);
|
||||||
|
}
|
||||||
|
|
||||||
|
transferTarget = previousContinuation.Context with { Rax = 0 };
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
if (!returnTarget.ThreadContinuation.HasValue ||
|
||||||
|
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
||||||
|
{
|
||||||
|
_continuations.TryRemove(fiberAddress, out _);
|
||||||
|
return ctx.SetReturn(FiberErrorState);
|
||||||
|
}
|
||||||
|
|
||||||
|
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("sceFiberReturnToThread", 0);
|
_currentFiberAddress = previousFiber;
|
||||||
return SetReturn(ctx, 0);
|
_ = GuestThreadExecution.EnterFiber(previousFiber);
|
||||||
|
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||||
|
TraceFiber(
|
||||||
|
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
||||||
|
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
||||||
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -281,18 +309,18 @@ public static class FiberExports
|
|||||||
var outAddress = ctx[CpuRegister.Rdi];
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||||
if (fiberAddress == 0)
|
if (fiberAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryWriteUInt64(ctx, outAddress, fiberAddress)
|
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorInvalid);
|
: ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -306,35 +334,35 @@ public static class FiberExports
|
|||||||
var info = ctx[CpuRegister.Rsi];
|
var info = ctx[CpuRegister.Rsi];
|
||||||
if (info == 0)
|
if (info == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, error);
|
return ctx.SetReturn(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadUInt64(ctx, info, out var size) || size != FiberInfoSize)
|
if (!ctx.TryReadUInt64(info, out var size) || size != FiberInfoSize)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
|
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
||||||
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
|
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
|
||||||
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
|
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
|
||||||
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
|
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
|
||||||
!TryWriteName(ctx, info + 40, fields.Name) ||
|
!TryWriteName(ctx, info + 40, fields.Name) ||
|
||||||
!TryWriteUInt64(ctx, info + 72, ulong.MaxValue))
|
!ctx.TryWriteUInt64(info + 72, ulong.MaxValue))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -348,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 SetReturn(ctx, error);
|
return ctx.SetReturn(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (nameAddress == 0)
|
if (nameAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
return TryWriteName(ctx, fiber + FiberNameOffset, name)
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorInvalid);
|
: ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -375,12 +403,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (ctx[CpuRegister.Rdi] != 0)
|
if (ctx[CpuRegister.Rdi] != 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorState);
|
: ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -391,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
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorState);
|
: ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -405,17 +433,17 @@ public static class FiberExports
|
|||||||
var outAddress = ctx[CpuRegister.Rdi];
|
var outAddress = ctx[CpuRegister.Rdi];
|
||||||
if (outAddress == 0)
|
if (outAddress == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (ResolveCurrentFiberAddress(ctx) == 0)
|
if (ResolveCurrentFiberAddress(ctx) == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
return TryWriteUInt64(ctx, outAddress, ctx[CpuRegister.Rbp])
|
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
||||||
? SetReturn(ctx, 0)
|
? ctx.SetReturn(0)
|
||||||
: SetReturn(ctx, FiberErrorInvalid);
|
: ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FiberInitializeCore(
|
private static int FiberInitializeCore(
|
||||||
@@ -431,37 +459,37 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorNull);
|
return ctx.SetReturn(FiberErrorNull);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((fiber & 7) != 0 ||
|
if ((fiber & 7) != 0 ||
|
||||||
(contextAddress & 15) != 0 ||
|
(contextAddress & 15) != 0 ||
|
||||||
(optParam & 7) != 0)
|
(optParam & 7) != 0)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorAlignment);
|
return ctx.SetReturn(FiberErrorAlignment);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorRange);
|
return ctx.SetReturn(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 SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (optParam != 0 &&
|
if (optParam != 0 &&
|
||||||
(!TryReadUInt32(ctx, optParam, out var optMagic) || optMagic != FiberOptSignature))
|
(!ctx.TryReadUInt32(optParam, out var optMagic) || optMagic != FiberOptSignature))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxNameLength + 1, out var name))
|
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
if (Volatile.Read(ref _contextSizeCheck) != 0)
|
||||||
@@ -469,27 +497,27 @@ public static class FiberExports
|
|||||||
flags |= FiberFlagContextSizeCheck;
|
flags |= FiberFlagContextSizeCheck;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
|
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||||
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
|
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
|
||||||
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
|
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
|
||||||
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||||
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
||||||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
||||||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
||||||
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
|
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
|
||||||
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
|
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
|
||||||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
||||||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||||
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
|
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (contextAddress != 0)
|
if (contextAddress != 0)
|
||||||
{
|
{
|
||||||
if (!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
if (!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if ((flags & FiberFlagContextSizeCheck) != 0)
|
if ((flags & FiberFlagContextSizeCheck) != 0)
|
||||||
@@ -504,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 SetReturn(ctx, 0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int FiberRunCore(
|
private static int FiberRunCore(
|
||||||
@@ -519,12 +547,12 @@ public static class FiberExports
|
|||||||
{
|
{
|
||||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, error);
|
return ctx.SetReturn(error);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (attachContextAddress != 0 || attachContextSize != 0)
|
if (attachContextAddress != 0 || attachContextSize != 0)
|
||||||
@@ -532,174 +560,156 @@ 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 SetReturn(ctx, attachResult);
|
return ctx.SetReturn(attachResult);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (fields.State != FiberStateIdle)
|
|
||||||
{
|
|
||||||
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
|
||||||
return SetReturn(ctx, FiberErrorState);
|
|
||||||
}
|
|
||||||
|
|
||||||
var previousFiber = ResolveCurrentFiberAddress(ctx);
|
var previousFiber = ResolveCurrentFiberAddress(ctx);
|
||||||
var switchingFromFiber = isSwitch && previousFiber != 0 && previousFiber != fiber;
|
if ((isSwitch && previousFiber == 0) ||
|
||||||
if (isSwitch && previousFiber == 0)
|
(!isSwitch && previousFiber != 0))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
|
}
|
||||||
|
if (previousFiber == fiber)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(FiberErrorState);
|
||||||
|
}
|
||||||
|
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||||
|
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(FiberErrorPermission);
|
||||||
}
|
}
|
||||||
|
|
||||||
var scheduler = GuestThreadExecution.Scheduler;
|
GuestCpuContinuation transferTarget;
|
||||||
if (scheduler is null)
|
var resumed = false;
|
||||||
|
lock (_fiberGate)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
if (!TryReadFiberFields(ctx, fiber, out fields))
|
||||||
}
|
|
||||||
|
|
||||||
var session = new FiberRunSession();
|
|
||||||
var ownsSession = _runSessions.TryAdd(fiber, session);
|
|
||||||
if (!ownsSession && !_runSessions.TryGetValue(fiber, out session))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, FiberErrorState);
|
|
||||||
}
|
|
||||||
|
|
||||||
FiberContinuation? previousContinuation = null;
|
|
||||||
GuestImportCallFrame switchFrame = default;
|
|
||||||
if (switchingFromFiber)
|
|
||||||
{
|
|
||||||
if (!GuestThreadExecution.TryGetCurrentImportCallFrame(out switchFrame))
|
|
||||||
{
|
{
|
||||||
if (ownsSession)
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
{
|
}
|
||||||
_runSessions.TryRemove(fiber, out _);
|
if (fields.State != FiberStateIdle)
|
||||||
}
|
{
|
||||||
TraceFiber($"switch-no-frame from=0x{previousFiber:X16} to=0x{fiber:X16} arg_out=0x{outArgumentAddress:X16}");
|
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
||||||
return SetReturn(ctx, FiberErrorPermission);
|
return ctx.SetReturn(FiberErrorState);
|
||||||
}
|
}
|
||||||
|
|
||||||
previousContinuation = new FiberContinuation(
|
FiberContinuation targetContinuation;
|
||||||
CaptureContinuation(ctx, switchFrame.ReturnRip, switchFrame.ResumeRsp, switchFrame.ReturnSlotAddress),
|
if (_continuations.TryGetValue(fiber, out var savedContinuation))
|
||||||
|
{
|
||||||
|
targetContinuation = savedContinuation;
|
||||||
|
resumed = true;
|
||||||
|
}
|
||||||
|
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
var callerContinuation = new FiberContinuation(
|
||||||
|
CaptureContinuation(ctx, frame.ReturnRip, frame.ResumeRsp, frame.ReturnSlotAddress),
|
||||||
outArgumentAddress);
|
outArgumentAddress);
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
|
if (previousFiber != 0)
|
||||||
{
|
|
||||||
if (ownsSession)
|
|
||||||
{
|
{
|
||||||
_runSessions.TryRemove(fiber, out _);
|
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
|
||||||
}
|
previousState != FiberStateRun ||
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
||||||
}
|
|
||||||
|
|
||||||
if (switchingFromFiber && !TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
|
|
||||||
{
|
|
||||||
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle);
|
|
||||||
if (ownsSession)
|
|
||||||
{
|
|
||||||
_runSessions.TryRemove(fiber, out _);
|
|
||||||
}
|
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (previousContinuation.HasValue)
|
|
||||||
{
|
|
||||||
_continuations[previousFiber] = previousContinuation.Value;
|
|
||||||
TraceFiber($"switch-save from=0x{previousFiber:X16} to=0x{fiber:X16} resume=0x{switchFrame.ReturnRip:X16} rsp=0x{switchFrame.ResumeRsp:X16} return_slot=0x{switchFrame.ReturnSlotAddress:X16} arg_out=0x{outArgumentAddress:X16}");
|
|
||||||
}
|
|
||||||
|
|
||||||
var previousReturnRequested = _fiberReturnRequested;
|
|
||||||
var previousReturnArgument = _fiberReturnArgument;
|
|
||||||
_currentFiberAddress = fiber;
|
|
||||||
_fiberReturnRequested = false;
|
|
||||||
_fiberReturnArgument = 0;
|
|
||||||
var previousExecutionFiber = GuestThreadExecution.EnterFiber(fiber);
|
|
||||||
TraceFiber($"run-enter reason={reason} fiber=0x{fiber:X16} prev=0x{previousFiber:X16} thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} managed={Environment.CurrentManagedThreadId} stack=0x{fields.ContextAddress:X16}");
|
|
||||||
|
|
||||||
var hasContinuation = _continuations.TryGetValue(fiber, out var continuation);
|
|
||||||
if (hasContinuation)
|
|
||||||
{
|
|
||||||
_continuations.TryRemove(fiber, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceFiber(hasContinuation
|
|
||||||
? $"run-dispatch reason={reason} fiber=0x{fiber:X16} resume=1 rip=0x{continuation.Context.Rip:X16} rsp=0x{continuation.Context.Rsp:X16} return_slot=0x{continuation.Context.ReturnSlotAddress:X16} arg_out=0x{continuation.ArgOnRunAddress:X16}"
|
|
||||||
: $"run-dispatch reason={reason} fiber=0x{fiber:X16} resume=0 rip=0x{fields.Entry:X16} rsp=0x{fields.ContextAddress + fields.ContextSize:X16} arg_out=0x{outArgumentAddress:X16}");
|
|
||||||
bool callbackOk;
|
|
||||||
string? callbackError;
|
|
||||||
try
|
|
||||||
{
|
|
||||||
if (hasContinuation)
|
|
||||||
{
|
|
||||||
if (continuation.ArgOnRunAddress != 0 &&
|
|
||||||
!TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argOnRun))
|
|
||||||
{
|
{
|
||||||
callbackOk = false;
|
return ctx.SetReturn(FiberErrorState);
|
||||||
callbackError = $"failed to write resumed argOnRun to 0x{continuation.ArgOnRunAddress:X16}";
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
callbackOk = scheduler.TryCallGuestContinuation(
|
|
||||||
ctx,
|
|
||||||
continuation.Context,
|
|
||||||
reason,
|
|
||||||
out callbackError);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_continuations[previousFiber] = callerContinuation;
|
||||||
|
_returnTargets[fiber] = new FiberReturnTarget(previousFiber, null);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
callbackOk = scheduler.TryCallGuestFunction(
|
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
||||||
ctx,
|
|
||||||
fields.Entry,
|
|
||||||
fields.ArgOnInitialize,
|
|
||||||
argOnRun,
|
|
||||||
fields.ContextAddress,
|
|
||||||
fields.ContextSize,
|
|
||||||
reason,
|
|
||||||
out callbackError);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
finally
|
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
|
||||||
{
|
{
|
||||||
GuestThreadExecution.RestoreFiber(previousExecutionFiber);
|
if (previousFiber != 0)
|
||||||
|
{
|
||||||
|
_continuations.TryRemove(previousFiber, out _);
|
||||||
|
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
||||||
|
}
|
||||||
|
_returnTargets.TryRemove(fiber, out _);
|
||||||
|
return ctx.SetReturn(FiberErrorInvalid);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (resumed)
|
||||||
|
{
|
||||||
|
_continuations.TryRemove(fiber, out _);
|
||||||
|
}
|
||||||
|
|
||||||
|
transferTarget = targetContinuation.Context with { Rax = 0 };
|
||||||
}
|
}
|
||||||
|
|
||||||
var returnRequested = _fiberReturnRequested;
|
_currentFiberAddress = fiber;
|
||||||
var returnArgument = _fiberReturnArgument;
|
_ = GuestThreadExecution.EnterFiber(fiber);
|
||||||
if (!returnRequested && session.TryGetReturn(out var sessionReturnArgument))
|
GuestThreadExecution.RequestCurrentContextTransfer(transferTarget);
|
||||||
{
|
TraceFiber(
|
||||||
returnRequested = true;
|
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
||||||
returnArgument = sessionReturnArgument;
|
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
||||||
}
|
return ctx.SetReturn(0);
|
||||||
|
|
||||||
if (ownsSession)
|
|
||||||
{
|
|
||||||
_runSessions.TryRemove(fiber, out _);
|
|
||||||
}
|
|
||||||
|
|
||||||
_currentFiberAddress = previousFiber;
|
|
||||||
_fiberReturnRequested = previousReturnRequested;
|
|
||||||
_fiberReturnArgument = previousReturnArgument;
|
|
||||||
|
|
||||||
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle);
|
|
||||||
if (switchingFromFiber)
|
|
||||||
{
|
|
||||||
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!callbackOk)
|
|
||||||
{
|
|
||||||
TraceFiber($"run-failed fiber=0x{fiber:X16} entry=0x{fields.Entry:X16} error={callbackError}");
|
|
||||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CPU_TRAP);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (outArgumentAddress != 0 && !TryWriteUInt64(ctx, outArgumentAddress, returnArgument))
|
|
||||||
{
|
|
||||||
return SetReturn(ctx, FiberErrorInvalid);
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceFiber($"run fiber=0x{fiber:X16} entry=0x{fields.Entry:X16} resume={hasContinuation} arg=0x{argOnRun:X16} ret=0x{returnArgument:X16}");
|
|
||||||
return SetReturn(ctx, 0);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryCreateInitialContinuation(
|
||||||
|
CpuContext ctx,
|
||||||
|
FiberFields fields,
|
||||||
|
ulong argOnRun,
|
||||||
|
out FiberContinuation continuation)
|
||||||
|
{
|
||||||
|
continuation = default;
|
||||||
|
if (fields.ContextAddress == 0 || fields.ContextSize < FiberContextMinimumSize)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
||||||
|
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
||||||
|
if (!ctx.TryWriteUInt64(entryRsp, 0))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
continuation = new FiberContinuation(
|
||||||
|
new GuestCpuContinuation(
|
||||||
|
fields.Entry,
|
||||||
|
entryRsp,
|
||||||
|
entryRsp,
|
||||||
|
ctx.Rflags == 0 ? 0x202UL : ctx.Rflags,
|
||||||
|
ctx.FsBase,
|
||||||
|
ctx.GsBase,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
argOnRun,
|
||||||
|
fields.ArgOnInitialize,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0,
|
||||||
|
0),
|
||||||
|
0);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteResumeArgument(
|
||||||
|
CpuContext ctx,
|
||||||
|
FiberContinuation continuation,
|
||||||
|
ulong argument) =>
|
||||||
|
continuation.ArgOnRunAddress == 0 ||
|
||||||
|
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
|
||||||
|
|
||||||
private static GuestCpuContinuation CaptureContinuation(
|
private static GuestCpuContinuation CaptureContinuation(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong resumeRip,
|
ulong resumeRip,
|
||||||
@@ -769,11 +779,11 @@ public static class FiberExports
|
|||||||
return FiberErrorInvalid;
|
return FiberErrorInvalid;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
||||||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
||||||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
||||||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||||
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||||
{
|
{
|
||||||
return FiberErrorInvalid;
|
return FiberErrorInvalid;
|
||||||
}
|
}
|
||||||
@@ -829,8 +839,8 @@ public static class FiberExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
|
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||||
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
|
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||||
magicStart != FiberSignature0 ||
|
magicStart != FiberSignature0 ||
|
||||||
magicEnd != FiberSignature1)
|
magicEnd != FiberSignature1)
|
||||||
{
|
{
|
||||||
@@ -845,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 (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
|
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
|
||||||
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
|
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
|
||||||
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||||
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
|
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||||
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
|
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||||
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
|
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
|
||||||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -888,46 +898,6 @@ 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];
|
||||||
@@ -955,37 +925,6 @@ 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))
|
||||||
@@ -1007,30 +946,9 @@ public static class FiberExports
|
|||||||
GuestCpuContinuation Context,
|
GuestCpuContinuation Context,
|
||||||
ulong ArgOnRunAddress);
|
ulong ArgOnRunAddress);
|
||||||
|
|
||||||
private sealed class FiberRunSession
|
private readonly record struct FiberReturnTarget(
|
||||||
{
|
ulong PreviousFiber,
|
||||||
private int _returnRequested;
|
FiberContinuation? ThreadContinuation);
|
||||||
|
|
||||||
private ulong _returnArgument;
|
|
||||||
|
|
||||||
public void SetReturn(ulong argument)
|
|
||||||
{
|
|
||||||
_returnArgument = argument;
|
|
||||||
Volatile.Write(ref _returnRequested, 1);
|
|
||||||
}
|
|
||||||
|
|
||||||
public bool TryGetReturn(out ulong argument)
|
|
||||||
{
|
|
||||||
if (Volatile.Read(ref _returnRequested) == 0)
|
|
||||||
{
|
|
||||||
argument = 0;
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
argument = _returnArgument;
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
private readonly record struct FiberStackRange(ulong Start, ulong Size)
|
private readonly record struct FiberStackRange(ulong Start, ulong Size)
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -0,0 +1,46 @@
|
|||||||
|
// 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,7 +3,6 @@
|
|||||||
|
|
||||||
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;
|
||||||
|
|
||||||
@@ -11,8 +10,13 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
|
|
||||||
public static class KernelAprCompatExports
|
public static class KernelAprCompatExports
|
||||||
{
|
{
|
||||||
private static readonly ConcurrentDictionary<uint, ulong> _submittedCommandBuffers = new();
|
private static readonly ConcurrentDictionary<uint, AprSubmission> _submittedCommandBuffers = new();
|
||||||
private static int _nextSubmissionId;
|
private static int _nextSubmissionId;
|
||||||
|
private static int _aprWaitTraceCount;
|
||||||
|
private static readonly bool _traceApr =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private readonly record struct AprSubmission(ulong CommandBuffer, ulong Priority, ulong ResultAddress);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ASoW5WE-UPo",
|
Nid = "ASoW5WE-UPo",
|
||||||
@@ -37,7 +41,7 @@ public static class KernelAprCompatExports
|
|||||||
submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
_submittedCommandBuffers[submissionId] = commandBuffer;
|
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, priority, resultAddress);
|
||||||
|
|
||||||
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
||||||
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
@@ -45,7 +49,7 @@ public static class KernelAprCompatExports
|
|||||||
return completionResult;
|
return completionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -67,20 +71,23 @@ public static class KernelAprCompatExports
|
|||||||
public static int KernelAprWaitCommandBuffer(CpuContext ctx)
|
public static int KernelAprWaitCommandBuffer(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
var submissionId = unchecked((uint)ctx[CpuRegister.Rdi]);
|
||||||
var priority = ctx[CpuRegister.Rsi];
|
var waitArg1 = ctx[CpuRegister.Rsi];
|
||||||
var resultAddress = ctx[CpuRegister.Rdx];
|
var waitArg2 = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
if (!_submittedCommandBuffers.TryRemove(submissionId, out var commandBuffer))
|
if (!_submittedCommandBuffers.TryRemove(submissionId, out var submission))
|
||||||
{
|
{
|
||||||
|
TraceAprWaitFailure(ctx, "wait_missing", submissionId, commandBuffer: 0, waitArg1, waitArg2);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var resultAddress = ResolveWaitResultAddress(waitArg1, waitArg2, submission.ResultAddress);
|
||||||
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
|
if (resultAddress != 0 && !TryWriteAprResult(ctx, resultAddress))
|
||||||
{
|
{
|
||||||
|
TraceAprWaitFailure(ctx, "wait_result_fault", submissionId, submission.CommandBuffer, waitArg1, waitArg2);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceApr(ctx, "wait", submissionId, commandBuffer, priority, resultAddress);
|
TraceApr(ctx, "wait", submissionId, submission.CommandBuffer, waitArg1, resultAddress);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -98,7 +105,7 @@ public static class KernelAprCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
||||||
_submittedCommandBuffers[submissionId] = commandBuffer;
|
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
|
||||||
|
|
||||||
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
||||||
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
@@ -125,7 +132,7 @@ public static class KernelAprCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
var submissionId = unchecked((uint)Interlocked.Increment(ref _nextSubmissionId));
|
||||||
_submittedCommandBuffers[submissionId] = commandBuffer;
|
_submittedCommandBuffers[submissionId] = new AprSubmission(commandBuffer, ctx[CpuRegister.Rsi], ResultAddress: 0);
|
||||||
|
|
||||||
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
var completionResult = AmprExports.CompleteCommandBuffer(ctx, commandBuffer);
|
||||||
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (completionResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
@@ -133,7 +140,7 @@ public static class KernelAprCompatExports
|
|||||||
return completionResult;
|
return completionResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
if (!ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -142,6 +149,19 @@ 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)];
|
||||||
@@ -149,11 +169,25 @@ public static class KernelAprCompatExports
|
|||||||
return ctx.Memory.TryWrite(resultAddress, result);
|
return ctx.Memory.TryWrite(resultAddress, result);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
private static ulong ResolveWaitResultAddress(ulong waitArg1, ulong waitArg2, ulong submittedResultAddress)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
if (waitArg2 == 0)
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
{
|
||||||
return ctx.Memory.TryWrite(address, buffer);
|
return submittedResultAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsAmprCompletionToken(waitArg1) && waitArg2 <= 0xFFFF)
|
||||||
|
{
|
||||||
|
return submittedResultAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
return waitArg2;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsAmprCompletionToken(ulong value)
|
||||||
|
{
|
||||||
|
var tag = value >> 56;
|
||||||
|
return tag is 0x0C or 0x10;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TraceApr(
|
private static void TraceApr(
|
||||||
@@ -164,7 +198,7 @@ public static class KernelAprCompatExports
|
|||||||
ulong priority,
|
ulong priority,
|
||||||
ulong aux)
|
ulong aux)
|
||||||
{
|
{
|
||||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal))
|
if (!_traceApr)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -181,4 +215,46 @@ public static class KernelAprCompatExports
|
|||||||
$"[LOADER][TRACE] apr.{operation}.result: addr=0x{aux:X16} q0=0x{result0:X16} q1=0x{result1:X16}");
|
$"[LOADER][TRACE] apr.{operation}.result: addr=0x{aux:X16} q0=0x{result0:X16} q1=0x{result1:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void TraceAprWaitFailure(
|
||||||
|
CpuContext ctx,
|
||||||
|
string operation,
|
||||||
|
uint submissionId,
|
||||||
|
ulong commandBuffer,
|
||||||
|
ulong priority,
|
||||||
|
ulong resultAddress)
|
||||||
|
{
|
||||||
|
if (!_traceApr)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var traceCount = Interlocked.Increment(ref _aprWaitTraceCount);
|
||||||
|
if (traceCount > 32 && (traceCount & 0x3FF) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var returnRip = 0UL;
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] apr.{operation}: id=0x{submissionId:X8} cmd=0x{commandBuffer:X16} " +
|
||||||
|
$"rsi=0x{priority:X16} rdx=0x{resultAddress:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16} " +
|
||||||
|
$"r8=0x{ctx[CpuRegister.R8]:X16} r9=0x{ctx[CpuRegister.R9]:X16} ret=0x{returnRip:X16}");
|
||||||
|
TraceReadableQword(ctx, operation, "rsi", priority);
|
||||||
|
TraceReadableQword(ctx, operation, "rdx", resultAddress);
|
||||||
|
TraceReadableQword(ctx, operation, "rcx", ctx[CpuRegister.Rcx]);
|
||||||
|
TraceReadableQword(ctx, operation, "r8", ctx[CpuRegister.R8]);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceReadableQword(CpuContext ctx, string operation, string name, ulong address)
|
||||||
|
{
|
||||||
|
if (address == 0 || !ctx.TryReadUInt64(address, out var value))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] apr.{operation}.{name}: addr=0x{address:X16} q0=0x{value:X16}");
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
// 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;
|
||||||
@@ -52,17 +51,17 @@ public static class KernelEventFlagCompatExports
|
|||||||
optionAddress != 0 ||
|
optionAddress != 0 ||
|
||||||
!IsValidAttributes(attributes))
|
!IsValidAttributes(attributes))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadNullTerminatedUtf8(ctx, nameAddress, MaxEventFlagNameLength + 1, out var name))
|
if (!ctx.TryReadNullTerminatedUtf8(nameAddress, MaxEventFlagNameLength + 1, out var name))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
if (Encoding.UTF8.GetByteCount(name) > MaxEventFlagNameLength)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
var handle = unchecked((ulong)Interlocked.Increment(ref _nextEventFlagHandle));
|
||||||
@@ -76,11 +75,11 @@ public static class KernelEventFlagCompatExports
|
|||||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||||
{
|
{
|
||||||
_eventFlags.TryRemove(handle, out _);
|
_eventFlags.TryRemove(handle, out _);
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -93,7 +92,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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
@@ -102,7 +101,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -117,7 +116,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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
@@ -127,7 +126,8 @@ public static class KernelEventFlagCompatExports
|
|||||||
TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
TraceEventFlag($"set handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} ret=0x{returnRip:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -141,7 +141,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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
@@ -150,7 +150,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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -167,29 +167,29 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
return ctx.SetReturn(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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -209,40 +209,61 @@ public static class KernelEventFlagCompatExports
|
|||||||
|
|
||||||
if (!_eventFlags.TryGetValue(handle, out var state))
|
if (!_eventFlags.TryGetValue(handle, out var state))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
if (pattern == 0 || !IsValidWaitMode(waitMode))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
uint timeoutUsec = 0;
|
uint timeoutUsec = 0;
|
||||||
if (timeoutAddress != 0 && !TryReadUInt32(ctx, timeoutAddress, out timeoutUsec))
|
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor.Enter(state.Gate);
|
Monitor.Enter(state.Gate);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, out var immediateWaitResult))
|
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, immediateWaitResult);
|
return ctx.SetReturn(immediateWaitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutAddress != 0)
|
if (timeoutAddress != 0)
|
||||||
{
|
{
|
||||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||||
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
var currentFiber = FiberExports.GetCurrentFiberAddressForDiagnostics(ctx);
|
||||||
var managedThread = Environment.CurrentManagedThreadId;
|
var managedThread = Environment.CurrentManagedThreadId;
|
||||||
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEventFlag");
|
var blockedWaitResult = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
var requestedBlock = GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"sceKernelWaitEventFlag",
|
||||||
|
GetEventFlagWakeKey(handle),
|
||||||
|
() => (int)blockedWaitResult,
|
||||||
|
() =>
|
||||||
|
{
|
||||||
|
if (!TryPrepareBlockedWait(
|
||||||
|
ctx,
|
||||||
|
state,
|
||||||
|
pattern,
|
||||||
|
waitMode,
|
||||||
|
resultAddress,
|
||||||
|
out var preparedResult))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
blockedWaitResult = preparedResult;
|
||||||
|
return true;
|
||||||
|
});
|
||||||
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
TraceEventFlag($"wait-unsatisfied handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} block={requestedBlock} ret=0x{returnRip:X16} frames={FormatFrameChain(ctx)}");
|
||||||
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
TraceEventFlag($"wait-object handle=0x{handle:X16} name='{state.Name}' {FormatGuestWaitObject(ctx)}");
|
||||||
if (!requestedBlock)
|
if (!requestedBlock)
|
||||||
@@ -250,7 +271,7 @@ public static class KernelEventFlagCompatExports
|
|||||||
var scheduler = GuestThreadExecution.Scheduler;
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
if (scheduler is null)
|
if (scheduler is null)
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.WaitingThreads++;
|
state.WaitingThreads++;
|
||||||
@@ -270,12 +291,12 @@ public static class KernelEventFlagCompatExports
|
|||||||
Monitor.Enter(state.Gate);
|
Monitor.Enter(state.Gate);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, out var pumpedWaitResult))
|
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var pumpedWaitResult))
|
||||||
{
|
{
|
||||||
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 SetReturn(ctx, pumpedWaitResult);
|
return ctx.SetReturn(pumpedWaitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
||||||
@@ -292,7 +313,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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
@@ -312,15 +333,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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
lock (state.Gate)
|
lock (state.Gate)
|
||||||
{
|
{
|
||||||
if (waiterCountAddress != 0 &&
|
if (waiterCountAddress != 0 &&
|
||||||
!TryWriteUInt32(ctx, waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
!ctx.TryWriteUInt32(waiterCountAddress, unchecked((uint)state.WaitingThreads)))
|
||||||
{
|
{
|
||||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
state.Bits = setPattern;
|
state.Bits = setPattern;
|
||||||
@@ -329,7 +350,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 SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool IsValidAttributes(uint attributes)
|
private static bool IsValidAttributes(uint attributes)
|
||||||
@@ -369,19 +390,21 @@ public static class KernelEventFlagCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryCompleteSatisfiedWait(
|
private static bool TryCompleteSatisfiedWait(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
EventFlagState state,
|
EventFlagState state,
|
||||||
ulong pattern,
|
ulong pattern,
|
||||||
uint waitMode,
|
uint waitMode,
|
||||||
out OrbisGen2Result result)
|
ulong resultAddress,
|
||||||
|
out OrbisGen2Result result)
|
||||||
{
|
{
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_OK;
|
result = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
|
||||||
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteResultPattern(ctx, ctx[CpuRegister.Rcx], state.Bits))
|
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
||||||
{
|
{
|
||||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
return true;
|
return true;
|
||||||
@@ -391,86 +414,43 @@ public static class KernelEventFlagCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
private static bool TryPrepareBlockedWait(
|
||||||
address == 0 || ctx.TryWriteUInt64(address, bits);
|
CpuContext ctx,
|
||||||
|
EventFlagState state,
|
||||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
ulong pattern,
|
||||||
|
uint waitMode,
|
||||||
|
ulong resultAddress,
|
||||||
|
out OrbisGen2Result result)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
lock (state.Gate)
|
||||||
if (!ctx.Memory.TryRead(address, buffer))
|
|
||||||
{
|
{
|
||||||
value = 0;
|
result = OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
return false;
|
if (!IsSatisfied(state.Bits, pattern, waitMode))
|
||||||
}
|
|
||||||
|
|
||||||
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;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (current[0] == 0)
|
if (!TryWriteResultPattern(ctx, resultAddress, state.Bits))
|
||||||
{
|
{
|
||||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
result = OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
return true;
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
ApplyClearMode(state, pattern, waitMode);
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes[index] = current[0];
|
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||||
|
TraceEventFlag(
|
||||||
|
$"wait-wake pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16} waiters={state.WaitingThreads}");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
value = Encoding.UTF8.GetString(bytes);
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
private static string GetEventFlagWakeKey(ulong handle) =>
|
||||||
{
|
$"event_flag:0x{handle:X16}";
|
||||||
var value = (int)result;
|
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
||||||
return value;
|
address == 0 || ctx.TryWriteUInt64(address, bits);
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceEventFlag(string message)
|
private static void TraceEventFlag(string message)
|
||||||
{
|
{
|
||||||
@@ -555,7 +535,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 (TryReadByte(ctx, address, out var value))
|
if (ctx.TryReadByte(address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X2}");
|
builder.Append($" {name}=0x{value:X2}");
|
||||||
}
|
}
|
||||||
@@ -563,7 +543,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 (TryReadUInt32(ctx, address, out var value))
|
if (ctx.TryReadUInt32(address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X8}");
|
builder.Append($" {name}=0x{value:X8}");
|
||||||
}
|
}
|
||||||
@@ -571,7 +551,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 (TryReadUInt64(ctx, address, out var value))
|
if (ctx.TryReadUInt64(address, out var value))
|
||||||
{
|
{
|
||||||
builder.Append($" {name}=0x{value:X16}");
|
builder.Append($" {name}=0x{value:X16}");
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,10 +10,14 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
public static class KernelEventQueueCompatExports
|
public static class KernelEventQueueCompatExports
|
||||||
{
|
{
|
||||||
private const int KernelEventSize = 0x20;
|
private const int KernelEventSize = 0x20;
|
||||||
|
public const short KernelEventFilterGraphics = -14;
|
||||||
|
public const short KernelEventFilterAmpr = -16;
|
||||||
|
public const short KernelEventFilterAmprSystem = -17;
|
||||||
|
|
||||||
private static readonly object _eventQueueGate = new();
|
private static readonly object _eventQueueGate = new();
|
||||||
private static readonly HashSet<ulong> _eventQueues = new();
|
private static readonly HashSet<ulong> _eventQueues = new();
|
||||||
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
|
private static readonly Dictionary<ulong, LinkedList<KernelQueuedEvent>> _pendingEvents = new();
|
||||||
|
private static readonly Dictionary<ulong, Dictionary<(ulong Ident, short Filter), KernelEventRegistration>> _registeredEvents = new();
|
||||||
private static long _nextEventQueueHandle = 1;
|
private static long _nextEventQueueHandle = 1;
|
||||||
|
|
||||||
public readonly record struct KernelQueuedEvent(
|
public readonly record struct KernelQueuedEvent(
|
||||||
@@ -24,6 +28,11 @@ public static class KernelEventQueueCompatExports
|
|||||||
ulong Data,
|
ulong Data,
|
||||||
ulong UserData);
|
ulong UserData);
|
||||||
|
|
||||||
|
private readonly record struct KernelEventRegistration(
|
||||||
|
ulong Ident,
|
||||||
|
short Filter,
|
||||||
|
ulong UserData);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "D0OdFMjp46I",
|
Nid = "D0OdFMjp46I",
|
||||||
ExportName = "sceKernelCreateEqueue",
|
ExportName = "sceKernelCreateEqueue",
|
||||||
@@ -42,6 +51,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
{
|
{
|
||||||
_eventQueues.Add(handle);
|
_eventQueues.Add(handle);
|
||||||
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
|
_pendingEvents[handle] = new LinkedList<KernelQueuedEvent>();
|
||||||
|
_registeredEvents[handle] = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||||
@@ -65,6 +75,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
{
|
{
|
||||||
_eventQueues.Remove(handle);
|
_eventQueues.Remove(handle);
|
||||||
_pendingEvents.Remove(handle);
|
_pendingEvents.Remove(handle);
|
||||||
|
_registeredEvents.Remove(handle);
|
||||||
}
|
}
|
||||||
|
|
||||||
TraceEventQueue(ctx, "delete", handle);
|
TraceEventQueue(ctx, "delete", handle);
|
||||||
@@ -122,8 +133,16 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelAddAmprEvent(CpuContext ctx)
|
public static int KernelAddAmprEvent(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "add_ampr", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var registered = RegisterEvent(
|
||||||
|
handle,
|
||||||
|
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||||
|
KernelEventFilterAmpr,
|
||||||
|
ctx[CpuRegister.Rdx]);
|
||||||
|
TraceEventQueue(ctx, "add_ampr", handle);
|
||||||
|
return registered
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -133,8 +152,16 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelAddAmprSystemEvent(CpuContext ctx)
|
public static int KernelAddAmprSystemEvent(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "add_ampr_system", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var registered = RegisterEvent(
|
||||||
|
handle,
|
||||||
|
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||||
|
KernelEventFilterAmprSystem,
|
||||||
|
ctx[CpuRegister.Rdx]);
|
||||||
|
TraceEventQueue(ctx, "add_ampr_system", handle);
|
||||||
|
return registered
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -144,8 +171,15 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelDeleteAmprEvent(CpuContext ctx)
|
public static int KernelDeleteAmprEvent(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "delete_ampr", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var deleted = DeleteRegisteredEvent(
|
||||||
|
handle,
|
||||||
|
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||||
|
KernelEventFilterAmpr);
|
||||||
|
TraceEventQueue(ctx, "delete_ampr", handle);
|
||||||
|
return deleted
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -155,8 +189,15 @@ public static class KernelEventQueueCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelDeleteAmprSystemEvent(CpuContext ctx)
|
public static int KernelDeleteAmprSystemEvent(CpuContext ctx)
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "delete_ampr_system", ctx[CpuRegister.Rdi]);
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var deleted = DeleteRegisteredEvent(
|
||||||
|
handle,
|
||||||
|
unchecked((uint)ctx[CpuRegister.Rsi]),
|
||||||
|
KernelEventFilterAmprSystem);
|
||||||
|
TraceEventQueue(ctx, "delete_ampr_system", handle);
|
||||||
|
return deleted
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -171,6 +212,57 @@ public static class KernelEventQueueCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "vz+pg2zdopI",
|
||||||
|
ExportName = "sceKernelGetEventUserData",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelGetEventUserData(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x18, out var userData);
|
||||||
|
ctx[CpuRegister.Rax] = userData;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mJ7aghmgvfc",
|
||||||
|
ExportName = "sceKernelGetEventId",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelGetEventId(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi], out var ident);
|
||||||
|
ctx[CpuRegister.Rax] = ident;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "23CPPI1tyBY",
|
||||||
|
ExportName = "sceKernelGetEventFilter",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelGetEventFilter(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Span<byte> filterBytes = stackalloc byte[sizeof(short)];
|
||||||
|
var filter = ctx.Memory.TryRead(ctx[CpuRegister.Rdi] + 0x08, filterBytes)
|
||||||
|
? BinaryPrimitives.ReadInt16LittleEndian(filterBytes)
|
||||||
|
: (short)0;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((uint)filter);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "kwGyyjohI50",
|
||||||
|
ExportName = "sceKernelGetEventData",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelGetEventData(CpuContext ctx)
|
||||||
|
{
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rdi] + 0x10, out var data);
|
||||||
|
ctx[CpuRegister.Rax] = data;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "fzyMKs9kim0",
|
Nid = "fzyMKs9kim0",
|
||||||
ExportName = "sceKernelWaitEqueue",
|
ExportName = "sceKernelWaitEqueue",
|
||||||
@@ -184,8 +276,24 @@ 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 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -196,12 +304,53 @@ public static class KernelEventQueueCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timeoutAddress == 0 && GuestThreadExecution.RequestCurrentThreadBlock("sceKernelWaitEqueue"))
|
if (timeoutAddress == 0 &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"sceKernelWaitEqueue",
|
||||||
|
GetEventQueueWakeKey(handle),
|
||||||
|
() => ResumeWaitEqueue(ctx, handle, eventsAddress, eventCapacity, outCountAddress),
|
||||||
|
() => HasPendingEvents(handle)))
|
||||||
{
|
{
|
||||||
TraceEventQueue(ctx, "wait-block", handle);
|
TraceEventQueue(ctx, "wait-block", handle);
|
||||||
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;
|
||||||
}
|
}
|
||||||
@@ -216,6 +365,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
|
|
||||||
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
|
public static bool EnqueueEvent(ulong handle, KernelQueuedEvent queuedEvent)
|
||||||
{
|
{
|
||||||
|
var queued = false;
|
||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
if (!_eventQueues.Contains(handle))
|
if (!_eventQueues.Contains(handle))
|
||||||
@@ -230,10 +380,101 @@ public static class KernelEventQueueCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
queue.AddLast(queuedEvent);
|
queue.AddLast(queuedEvent);
|
||||||
|
queued = true;
|
||||||
|
Monitor.PulseAll(_eventQueueGate);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (queued)
|
||||||
|
{
|
||||||
|
WakeEventQueue(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return queued;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static bool RegisterEvent(
|
||||||
|
ulong handle,
|
||||||
|
ulong ident,
|
||||||
|
short filter,
|
||||||
|
ulong userData)
|
||||||
|
{
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
if (!_eventQueues.Contains(handle))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_registeredEvents.TryGetValue(handle, out var events))
|
||||||
|
{
|
||||||
|
events = new Dictionary<(ulong Ident, short Filter), KernelEventRegistration>();
|
||||||
|
_registeredEvents[handle] = events;
|
||||||
|
}
|
||||||
|
|
||||||
|
events[(ident, filter)] = new KernelEventRegistration(ident, filter, userData);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public static bool DeleteRegisteredEvent(
|
||||||
|
ulong handle,
|
||||||
|
ulong ident,
|
||||||
|
short filter)
|
||||||
|
{
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
return _registeredEvents.TryGetValue(handle, out var events) &&
|
||||||
|
events.Remove((ident, filter));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static int TriggerRegisteredEvents(
|
||||||
|
ulong ident,
|
||||||
|
short filter,
|
||||||
|
ulong data)
|
||||||
|
{
|
||||||
|
List<ulong>? wakeHandles = null;
|
||||||
|
var triggeredCount = 0;
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
foreach (var (handle, registrations) in _registeredEvents)
|
||||||
|
{
|
||||||
|
if (!registrations.TryGetValue((ident, filter), out var registration))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!_pendingEvents.TryGetValue(handle, out var queue))
|
||||||
|
{
|
||||||
|
queue = new LinkedList<KernelQueuedEvent>();
|
||||||
|
_pendingEvents[handle] = queue;
|
||||||
|
}
|
||||||
|
|
||||||
|
QueueOrUpdateEvent(
|
||||||
|
queue,
|
||||||
|
new KernelQueuedEvent(
|
||||||
|
registration.Ident,
|
||||||
|
registration.Filter,
|
||||||
|
0,
|
||||||
|
1,
|
||||||
|
data,
|
||||||
|
registration.UserData));
|
||||||
|
(wakeHandles ??= new List<ulong>()).Add(handle);
|
||||||
|
triggeredCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (wakeHandles is not null)
|
||||||
|
{
|
||||||
|
foreach (var handle in wakeHandles)
|
||||||
|
{
|
||||||
|
WakeEventQueue(handle);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggeredCount;
|
||||||
|
}
|
||||||
|
|
||||||
public static bool TriggerDisplayEvent(
|
public static bool TriggerDisplayEvent(
|
||||||
ulong handle,
|
ulong handle,
|
||||||
ulong ident,
|
ulong ident,
|
||||||
@@ -241,6 +482,7 @@ public static class KernelEventQueueCompatExports
|
|||||||
ulong eventHint,
|
ulong eventHint,
|
||||||
ulong userData)
|
ulong userData)
|
||||||
{
|
{
|
||||||
|
var triggered = false;
|
||||||
lock (_eventQueueGate)
|
lock (_eventQueueGate)
|
||||||
{
|
{
|
||||||
if (!_eventQueues.Contains(handle))
|
if (!_eventQueues.Contains(handle))
|
||||||
@@ -254,17 +496,8 @@ public static class KernelEventQueueCompatExports
|
|||||||
_pendingEvents[handle] = events;
|
_pendingEvents[handle] = events;
|
||||||
}
|
}
|
||||||
|
|
||||||
LinkedListNode<KernelQueuedEvent>? pendingNode = null;
|
|
||||||
for (var node = events.First; node is not null; node = node.Next)
|
|
||||||
{
|
|
||||||
if (node.Value.Ident == ident && node.Value.Filter == filter)
|
|
||||||
{
|
|
||||||
pendingNode = node;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
var count = 1UL;
|
var count = 1UL;
|
||||||
|
var pendingNode = FindPendingEvent(events, ident, filter);
|
||||||
if (pendingNode is not null)
|
if (pendingNode is not null)
|
||||||
{
|
{
|
||||||
count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL);
|
count = Math.Min(((pendingNode.Value.Data >> 12) & 0xFUL) + 1, 0xFUL);
|
||||||
@@ -289,8 +522,82 @@ public static class KernelEventQueueCompatExports
|
|||||||
events.AddLast(triggeredEvent);
|
events.AddLast(triggeredEvent);
|
||||||
}
|
}
|
||||||
|
|
||||||
return true;
|
triggered = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (triggered)
|
||||||
|
{
|
||||||
|
WakeEventQueue(handle);
|
||||||
|
}
|
||||||
|
|
||||||
|
return triggered;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ResumeWaitEqueue(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong handle,
|
||||||
|
ulong eventsAddress,
|
||||||
|
int eventCapacity,
|
||||||
|
ulong outCountAddress)
|
||||||
|
{
|
||||||
|
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||||
|
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return deliveredCount > 0
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool HasPendingEvents(ulong handle)
|
||||||
|
{
|
||||||
|
lock (_eventQueueGate)
|
||||||
|
{
|
||||||
|
return _pendingEvents.TryGetValue(handle, out var events) && events.Count != 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void QueueOrUpdateEvent(
|
||||||
|
LinkedList<KernelQueuedEvent> queue,
|
||||||
|
KernelQueuedEvent queuedEvent)
|
||||||
|
{
|
||||||
|
var pendingNode = FindPendingEvent(queue, queuedEvent.Ident, queuedEvent.Filter);
|
||||||
|
if (pendingNode is null)
|
||||||
|
{
|
||||||
|
queue.AddLast(queuedEvent);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingNode.Value = queuedEvent with
|
||||||
|
{
|
||||||
|
Fflags = Math.Max(pendingNode.Value.Fflags + 1, queuedEvent.Fflags),
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
private static LinkedListNode<KernelQueuedEvent>? FindPendingEvent(
|
||||||
|
LinkedList<KernelQueuedEvent> queue,
|
||||||
|
ulong ident,
|
||||||
|
short filter)
|
||||||
|
{
|
||||||
|
for (var node = queue.First; node is not null; node = node.Next)
|
||||||
|
{
|
||||||
|
if (node.Value.Ident == ident && node.Value.Filter == filter)
|
||||||
|
{
|
||||||
|
return node;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetEventQueueWakeKey(ulong handle) =>
|
||||||
|
$"sceKernelWaitEqueue:{handle:X16}";
|
||||||
|
|
||||||
|
private static void WakeEventQueue(ulong handle)
|
||||||
|
{
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventQueueWakeKey(handle));
|
||||||
}
|
}
|
||||||
|
|
||||||
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
|
private static int DequeueEvents(CpuContext ctx, ulong handle, ulong eventsAddress, int eventCapacity)
|
||||||
@@ -347,16 +654,8 @@ public static class KernelEventQueueCompatExports
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
var returnRip = 0UL;
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
|
||||||
_ = 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);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,65 @@
|
|||||||
|
// 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,12 +8,13 @@ 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,
|
||||||
@@ -27,7 +28,24 @@ public static class KernelExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
||||||
{
|
{
|
||||||
_ = ctx;
|
var versionAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (versionAddress == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sdkVersion = ctx.TargetGeneration == Generation.Gen5
|
||||||
|
? Gen5CompiledSdkVersion
|
||||||
|
: Gen4CompiledSdkVersion;
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt32(versionAddress, sdkVersion))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -53,12 +71,20 @@ 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)
|
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "L1SBTkC+Cvw",
|
||||||
|
ExportName = "abort",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Abort(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
// Route through the same graceful guest-entry-exit path as exit(): letting the call
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
|
// fall through to the host's native abort() does not unwind the guest thread cleanly.
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
|
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -173,11 +199,7 @@ 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)
|
public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
|
||||||
{
|
|
||||||
_ = ctx;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Ac86z8q7T8A",
|
Nid = "Ac86z8q7T8A",
|
||||||
@@ -204,6 +226,16 @@ public static class KernelExports
|
|||||||
var nameAddress = ctx[CpuRegister.R8];
|
var nameAddress = ctx[CpuRegister.R8];
|
||||||
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
|
var name = nameAddress == 0 ? string.Empty : ReadCString(ctx, nameAddress, 256);
|
||||||
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
|
var threadHandle = KernelPthreadState.CreateThreadHandle(name);
|
||||||
|
KernelPthreadExtendedCompatExports.GetThreadStartScheduling(
|
||||||
|
ctx,
|
||||||
|
attrAddress,
|
||||||
|
out var priority,
|
||||||
|
out var affinityMask);
|
||||||
|
KernelPthreadExtendedCompatExports.RegisterThreadStart(
|
||||||
|
threadHandle,
|
||||||
|
name,
|
||||||
|
priority,
|
||||||
|
affinityMask);
|
||||||
if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, threadHandle))
|
if (threadIdAddress != 0 && !ctx.TryWriteUInt64(threadIdAddress, threadHandle))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
@@ -212,13 +244,22 @@ public static class KernelExports
|
|||||||
if (ShouldTracePthread())
|
if (ShouldTracePthread())
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} name='{name}' -> thread=0x{threadHandle:X16}");
|
$"[LOADER][TRACE] pthread_create: out=0x{threadIdAddress:X16} attr=0x{attrAddress:X16} " +
|
||||||
|
$"entry=0x{entryAddress:X16} arg=0x{argument:X16} name_ptr=0x{nameAddress:X16} " +
|
||||||
|
$"name='{name}' priority={priority} affinity=0x{affinityMask:X} -> thread=0x{threadHandle:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
var scheduler = GuestThreadExecution.Scheduler;
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
if (scheduler is not null && entryAddress != 0)
|
if (scheduler is not null && entryAddress != 0)
|
||||||
{
|
{
|
||||||
var request = new GuestThreadStartRequest(threadHandle, entryAddress, argument, attrAddress, name);
|
var request = new GuestThreadStartRequest(
|
||||||
|
threadHandle,
|
||||||
|
entryAddress,
|
||||||
|
argument,
|
||||||
|
attrAddress,
|
||||||
|
name,
|
||||||
|
priority,
|
||||||
|
affinityMask);
|
||||||
if (!scheduler.TryStartThread(ctx, request, out var error))
|
if (!scheduler.TryStartThread(ctx, request, out var error))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
@@ -237,19 +278,39 @@ 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)
|
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(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)
|
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3kg7rT0NQIs",
|
||||||
|
ExportName = "scePthreadExit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadExit(CpuContext ctx)
|
||||||
{
|
{
|
||||||
return PthreadCreate(ctx);
|
var value = ctx[CpuRegister.Rdi];
|
||||||
|
GuestThreadExecution.RequestCurrentEntryExit("scePthreadExit", value);
|
||||||
|
ctx[CpuRegister.Rax] = value;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "FJrT5LuUBAU",
|
||||||
|
ExportName = "pthread_exit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libScePosix")]
|
||||||
|
public static int PosixPthreadExit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var value = ctx[CpuRegister.Rdi];
|
||||||
|
GuestThreadExecution.RequestCurrentEntryExit("pthread_exit", value);
|
||||||
|
ctx[CpuRegister.Rax] = value;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -261,10 +322,6 @@ public static class KernelExports
|
|||||||
{
|
{
|
||||||
var threadId = ctx[CpuRegister.Rdi];
|
var threadId = ctx[CpuRegister.Rdi];
|
||||||
var returnValueAddress = ctx[CpuRegister.Rsi];
|
var returnValueAddress = ctx[CpuRegister.Rsi];
|
||||||
if (returnValueAddress != 0 && !ctx.TryWriteUInt64(returnValueAddress, 0))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (ShouldTracePthread())
|
if (ShouldTracePthread())
|
||||||
{
|
{
|
||||||
@@ -272,6 +329,30 @@ public static class KernelExports
|
|||||||
$"[LOADER][TRACE] pthread_join: thread=0x{threadId:X16} retval_out=0x{returnValueAddress:X16}");
|
$"[LOADER][TRACE] pthread_join: thread=0x{threadId:X16} retval_out=0x{returnValueAddress:X16}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var returnValue = 0UL;
|
||||||
|
if (GuestThreadExecution.Scheduler is { } scheduler &&
|
||||||
|
!scheduler.TryJoinThread(ctx, threadId, out returnValue, out var error))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] pthread_join: thread=0x{threadId:X16}: {error}");
|
||||||
|
var result = string.Equals(
|
||||||
|
error,
|
||||||
|
"thread cannot join itself",
|
||||||
|
StringComparison.Ordinal)
|
||||||
|
? OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT
|
||||||
|
: OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (returnValueAddress != 0 &&
|
||||||
|
!ctx.TryWriteUInt64(returnValueAddress, returnValue))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] =
|
||||||
|
unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -281,10 +362,7 @@ 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)
|
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
|
||||||
{
|
|
||||||
return PthreadJoin(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "wuCroIGjt2g",
|
Nid = "wuCroIGjt2g",
|
||||||
@@ -300,6 +378,13 @@ 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",
|
||||||
@@ -366,4 +451,76 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||