mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-26 04:39:17 +08:00
Compare commits
96 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b522776988 | |||
| 4bd42795c7 | |||
| c03ca32a02 | |||
| 6e2878f2ff | |||
| 2e09b015bc | |||
| 7b9efd1539 | |||
| 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 |
+1
-1
@@ -14,7 +14,7 @@ indent_size = 4
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
|
||||
[*.{md,json,yml,props,csproj}]
|
||||
[*.{md,json,yml,xml,props,csproj}]
|
||||
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
|
||||
@@ -16,6 +16,11 @@ body:
|
||||
Example title:
|
||||
Demon’s Souls (PPSA01341)
|
||||
|
||||
**Before reporting:**
|
||||
- Test the game on a **fresh build from `main`**, not an old release.
|
||||
- Copy the **exact commit SHA** shown at the top of the log.
|
||||
- `DEBUG` and `TRACE` lines are diagnostic context, not errors by themselves; report the last milestone or the first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop.
|
||||
|
||||
- type: input
|
||||
id: game_name
|
||||
attributes:
|
||||
@@ -73,11 +78,20 @@ body:
|
||||
id: logs
|
||||
attributes:
|
||||
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:
|
||||
required: false
|
||||
required: true
|
||||
accept: ".log,.txt,.zip"
|
||||
|
||||
- type: input
|
||||
id: last_milestone_first_error
|
||||
attributes:
|
||||
label: Last Milestone / First Error
|
||||
description: Paste the **last useful milestone line** or the **first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop** from the log. `DEBUG`/`TRACE` lines do not count unless a real error follows them.
|
||||
placeholder: "e.g. CpuEngine: native-only OR [ERROR] Native backend FAILED: ..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: operating_system
|
||||
attributes:
|
||||
@@ -119,7 +133,8 @@ body:
|
||||
id: emulator_version
|
||||
attributes:
|
||||
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:
|
||||
required: true
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 225 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB |
@@ -9,11 +9,17 @@ on:
|
||||
- "**"
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "**/*.png"
|
||||
- "**/*.jpeg"
|
||||
- "**/*.jpg"
|
||||
pull_request:
|
||||
branches:
|
||||
- main
|
||||
paths-ignore:
|
||||
- "**/*.md"
|
||||
- "**/*.png"
|
||||
- "**/*.jpeg"
|
||||
- "**/*.jpg"
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
@@ -120,7 +126,7 @@ jobs:
|
||||
needs:
|
||||
- init
|
||||
- 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
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -26,6 +26,7 @@ arm64/
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
.packages
|
||||
packages/
|
||||
*.nupkg
|
||||
.nuget/
|
||||
@@ -36,3 +37,6 @@ packages/
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
ehthumbs.db
|
||||
|
||||
.vs/
|
||||
.idea/
|
||||
|
||||
@@ -32,4 +32,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<DebugType>none</DebugType>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Build provenance surfaced by SharpEmu.Logging.BuildInfo as a banner at the
|
||||
top of the log. Populated from GitHub Actions environment variables when
|
||||
building in CI; empty locally. -->
|
||||
<ItemGroup>
|
||||
<AssemblyMetadata Include="SharpEmu.BuildConfiguration" Value="$(Configuration)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_SHA)' != ''" Include="SharpEmu.BuildSha" Value="$(GITHUB_SHA)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_REF_NAME)' != ''" Include="SharpEmu.BuildBranch" Value="$(GITHUB_REF_NAME)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_REF)' != ''" Include="SharpEmu.BuildRef" Value="$(GITHUB_REF)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_EVENT_NAME)' != ''" Include="SharpEmu.BuildEventName" Value="$(GITHUB_EVENT_NAME)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_REPOSITORY)' != ''" Include="SharpEmu.BuildRepository" Value="$(GITHUB_REPOSITORY)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_SERVER_URL)' != ''" Include="SharpEmu.BuildServerUrl" Value="$(GITHUB_SERVER_URL)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_RUN_ID)' != ''" Include="SharpEmu.BuildRunId" Value="$(GITHUB_RUN_ID)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<!--
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -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.
|
||||
+9
-1
@@ -3,13 +3,21 @@ version = 1
|
||||
[[annotations]]
|
||||
path = [
|
||||
"REUSE.toml",
|
||||
"nuget.config",
|
||||
"global.json",
|
||||
"**/packages.lock.json",
|
||||
"scripts/ps5_names.txt",
|
||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||
"_logs/**",
|
||||
".github/images/**"
|
||||
".github/images/**",
|
||||
"assets/images/**"
|
||||
]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||
SPDX-License-Identifier = "GPL-2.0-or-later"
|
||||
|
||||
[[annotations]]
|
||||
path = "src/SharpEmu.GUI/Atrac9/**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2018 Alex Barney"
|
||||
SPDX-License-Identifier = "MIT"
|
||||
|
||||
@@ -7,6 +7,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
|
||||
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
|
||||
<Project Path="src/SharpEmu.GUI/SharpEmu.GUI.csproj" />
|
||||
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
|
||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
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>
|
||||
+160
-2
@@ -3,6 +3,7 @@
|
||||
|
||||
using SharpEmu.Core.Runtime;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.GUI;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
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_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
private const int ATTACH_PARENT_PROCESS = -1;
|
||||
private const int STD_OUTPUT_HANDLE = -11;
|
||||
private const int STD_ERROR_HANDLE = -12;
|
||||
private const uint GENERIC_READ = 0x80000000;
|
||||
private const uint GENERIC_WRITE = 0x40000000;
|
||||
private const uint FILE_SHARE_READ = 0x00000001;
|
||||
private const uint FILE_SHARE_WRITE = 0x00000002;
|
||||
private const uint OPEN_EXISTING = 3;
|
||||
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Run(args);
|
||||
}
|
||||
finally
|
||||
{
|
||||
SharpEmuLog.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private static int Run(string[] args)
|
||||
{
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
if (args.Length == 0 && !isMitigatedChild)
|
||||
{
|
||||
// No arguments: open the desktop frontend. Any argument selects
|
||||
// the classic CLI behavior below.
|
||||
return GuiLauncher.Run();
|
||||
}
|
||||
|
||||
// The executable uses the GUI subsystem, so CLI mode has to connect
|
||||
// itself to a console before the first write.
|
||||
EnsureCliConsole();
|
||||
UseUtf8ConsoleOutput();
|
||||
|
||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
||||
{
|
||||
return childExitCode;
|
||||
@@ -43,9 +77,11 @@ internal static partial class Program
|
||||
|
||||
SharpEmuLog.MinimumLevel = logLevel;
|
||||
|
||||
Log.Info(BuildInfo.Banner);
|
||||
|
||||
ebootPath = Path.GetFullPath(ebootPath);
|
||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||
|
||||
|
||||
if (!File.Exists(ebootPath))
|
||||
{
|
||||
Log.Error($"EBOOT file was not found: {ebootPath}");
|
||||
@@ -101,6 +137,100 @@ internal static partial class Program
|
||||
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
|
||||
}
|
||||
|
||||
private static void EnsureCliConsole()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard handles already provided (pipes or file redirection, e.g.
|
||||
// when the GUI or a script launches us): use them as-is.
|
||||
if (IsHandleValid(GetStdHandle(STD_OUTPUT_HANDLE)) && IsHandleValid(GetStdHandle(STD_ERROR_HANDLE)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer the console of the parent process (interactive terminal);
|
||||
// create one only when started with arguments but no terminal at all
|
||||
// (e.g. a shortcut), so usage and errors remain visible.
|
||||
if (!AttachConsole(ATTACH_PARENT_PROCESS) && GetConsoleWindow() == 0)
|
||||
{
|
||||
_ = AllocConsole();
|
||||
}
|
||||
|
||||
RebindStdHandleToConsole(STD_OUTPUT_HANDLE);
|
||||
RebindStdHandleToConsole(STD_ERROR_HANDLE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes console writes UTF-8 so the GUI's pipe reader (and any modern
|
||||
/// terminal) decodes non-ASCII text correctly. Without this, redirected
|
||||
/// output falls back to the OS ANSI code page and characters like "—"
|
||||
/// arrive mangled.
|
||||
/// </summary>
|
||||
private static void UseUtf8ConsoleOutput()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Also recreates the redirected Console.Out/Error writers with
|
||||
// the new encoding.
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// No attached console (GUI-subsystem child with piped output):
|
||||
// wrap the raw handles instead.
|
||||
}
|
||||
|
||||
if (Console.IsOutputRedirected)
|
||||
{
|
||||
Console.SetOut(new StreamWriter(
|
||||
Console.OpenStandardOutput(),
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
|
||||
{
|
||||
AutoFlush = true,
|
||||
});
|
||||
}
|
||||
|
||||
if (Console.IsErrorRedirected)
|
||||
{
|
||||
Console.SetError(new StreamWriter(
|
||||
Console.OpenStandardError(),
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
|
||||
{
|
||||
AutoFlush = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void RebindStdHandleToConsole(int stdHandle)
|
||||
{
|
||||
if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var conOut = CreateFileW(
|
||||
"CONOUT$",
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
0,
|
||||
OPEN_EXISTING,
|
||||
0,
|
||||
0);
|
||||
if (IsHandleValid(conOut))
|
||||
{
|
||||
_ = SetStdHandle(stdHandle, conOut);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsHandleValid(nint handle)
|
||||
{
|
||||
return handle != 0 && handle != -1;
|
||||
}
|
||||
|
||||
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
||||
{
|
||||
isMitigatedChild = false;
|
||||
@@ -670,4 +800,32 @@ internal static partial class Program
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(nint handle);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern nint GetConsoleWindow();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AttachConsole(int processId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AllocConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint GetStdHandle(int stdHandle);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetStdHandle(int stdHandle, nint handle);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern nint CreateFileW(
|
||||
string fileName,
|
||||
uint desiredAccess,
|
||||
uint shareMode,
|
||||
nint securityAttributes,
|
||||
uint creationDisposition,
|
||||
uint flagsAndAttributes,
|
||||
nint templateFile);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<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>
|
||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
|
||||
<SelfContained>true</SelfContained>
|
||||
@@ -30,6 +33,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
|
||||
<ApplicationIcon>..\..\assets\images\SharpEmu.ico</ApplicationIcon>
|
||||
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
@@ -38,4 +50,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||
library is embedded into the single-file bundle. -->
|
||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
||||
<ItemGroup>
|
||||
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
|
||||
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw'))" />
|
||||
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
|
||||
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,6 +8,105 @@
|
||||
"resolved": "10.0.3",
|
||||
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||
},
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.BuildServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.2",
|
||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||
},
|
||||
"Avalonia.FreeDesktop": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Tmds.DBus.Protocol": "0.21.3"
|
||||
}
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Remote.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
||||
},
|
||||
"Avalonia.Skia": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"HarfBuzzSharp": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
||||
"SkiaSharp": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
||||
}
|
||||
},
|
||||
"Avalonia.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
||||
}
|
||||
},
|
||||
"Avalonia.X11": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.FreeDesktop": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
||||
"dependencies": {
|
||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"MicroCom.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.11.0",
|
||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
@@ -59,6 +158,38 @@
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"SkiaSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||
"dependencies": {
|
||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
@@ -73,14 +204,29 @@
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.gui": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Avalonia": "[11.3.18, )",
|
||||
"Avalonia.Desktop": "[11.3.18, )",
|
||||
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
||||
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
||||
"SharpEmu.Logging": "[1.0.0, )",
|
||||
"Tmds.DBus.Protocol": "[0.21.3, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
@@ -88,6 +234,48 @@
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Avalonia": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "11.3.18",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Native": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18",
|
||||
"Avalonia.Win32": "11.3.18",
|
||||
"Avalonia.X11": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Iced": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[1.21.0, )",
|
||||
@@ -103,6 +291,16 @@
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
@@ -122,9 +320,61 @@
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[0.21.3, )",
|
||||
"resolved": "0.21.3",
|
||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||
}
|
||||
},
|
||||
"net10.0/linux-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
@@ -132,6 +382,52 @@
|
||||
}
|
||||
},
|
||||
"net10.0/osx-arm64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
@@ -139,6 +435,52 @@
|
||||
}
|
||||
},
|
||||
"net10.0/win-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
|
||||
@@ -7,11 +7,14 @@ using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu;
|
||||
|
||||
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
|
||||
|
||||
private enum EntryFrameKind
|
||||
{
|
||||
ProcessEntry,
|
||||
@@ -80,8 +83,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string processImageName = "eboot.bin",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
Log.Debug("=== DispatchEntry START ===");
|
||||
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -89,8 +92,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -103,8 +105,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string moduleName = "module",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
Log.Debug("=== DispatchModuleInitializer START ===");
|
||||
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -119,8 +121,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -134,7 +135,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
CpuExecutionOptions executionOptions = default,
|
||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
||||
Log.Debug("DispatchEntryCore STARTING...");
|
||||
|
||||
LastEntryPoint = entryPoint;
|
||||
LastTrapInfo = null;
|
||||
@@ -306,7 +307,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
LastMilestoneLog,
|
||||
Environment.NewLine,
|
||||
$"CpuEngine native-only failed: {backendError}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
||||
Log.Error($"Native backend FAILED: {backendError}");
|
||||
return FailEarly(
|
||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||
CpuExitReason.NativeBackendUnavailable);
|
||||
@@ -415,8 +416,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
|
||||
const ulong entryParamsSize = 0x20;
|
||||
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
||||
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
|
||||
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
|
||||
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
|
||||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
||||
@@ -456,13 +457,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
return value & ~(alignment - 1);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return context.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static string BuildEntryFrameDiagnostic(
|
||||
ulong entryPoint,
|
||||
CpuContext context,
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
@@ -43,7 +44,7 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
return;
|
||||
}
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Recent import calls ({_recentImportTraceCount}):");
|
||||
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
|
||||
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
|
||||
for (int i = 0; i < _recentImportTraceCount; i++)
|
||||
{
|
||||
@@ -51,8 +52,8 @@ public sealed partial class DirectExecutionBackend
|
||||
var entry = _recentImportTrace[num2];
|
||||
if (!string.IsNullOrEmpty(entry.Nid))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -91,12 +92,12 @@ public sealed partial class DirectExecutionBackend
|
||||
list.Add(num7);
|
||||
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++;
|
||||
}
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -106,7 +107,7 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
if (num != 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer hits: {num}");
|
||||
Log.Warning($"Suspicious unresolved pointer hits: {num}");
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -121,18 +122,18 @@ public sealed partial class DirectExecutionBackend
|
||||
Span<byte> destination = stackalloc byte[128];
|
||||
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;
|
||||
}
|
||||
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))
|
||||
{
|
||||
int num = BitConverter.ToInt32(destination.Slice(2, 4));
|
||||
ulong num2 = returnRip + 6 + (ulong)num;
|
||||
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)
|
||||
@@ -141,7 +142,7 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong num4 = returnRip + 7 + (ulong)num3;
|
||||
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++)
|
||||
@@ -153,7 +154,7 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong num7 = num6 + 6 + (ulong)num5;
|
||||
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}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,7 +169,7 @@ public sealed partial class DirectExecutionBackend
|
||||
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
|
||||
ulong callRip = returnRip + (ulong)i;
|
||||
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
||||
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++)
|
||||
{
|
||||
if (_importEntries[importIndex].Address != target)
|
||||
@@ -179,21 +180,21 @@ public sealed partial class DirectExecutionBackend
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (cpuContext.Memory.TryRead(target, targetBytes))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
||||
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
|
||||
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
|
||||
{
|
||||
@@ -201,8 +202,8 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
|
||||
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||
{
|
||||
if (_importEntries[importIndex].Address != slotTarget)
|
||||
@@ -213,13 +214,13 @@ public sealed partial class DirectExecutionBackend
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,15 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||
|
||||
// Thread-mode probe: a hardware exception raised while this thread is inside
|
||||
// the managed import gateway means the VEH->managed reentry happened from
|
||||
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
|
||||
if (LogThreadMode && _threadModeGatewayDepth > 0)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
||||
}
|
||||
|
||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
{
|
||||
return -1;
|
||||
|
||||
@@ -19,6 +19,16 @@ public sealed partial class DirectExecutionBackend
|
||||
|
||||
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeGatewayCalls++;
|
||||
_threadModeGatewayDepth++;
|
||||
if (!_threadModeGatewayFirstLogged)
|
||||
{
|
||||
_threadModeGatewayFirstLogged = true;
|
||||
TraceThreadMode($"gateway_first import={importIndex} total={_threadModeGatewayCalls}");
|
||||
}
|
||||
}
|
||||
try
|
||||
{
|
||||
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
|
||||
@@ -36,6 +46,13 @@ public sealed partial class DirectExecutionBackend
|
||||
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
|
||||
return 18446744071562199298uL;
|
||||
}
|
||||
finally
|
||||
{
|
||||
if (LogThreadMode)
|
||||
{
|
||||
_threadModeGatewayDepth--;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
|
||||
@@ -69,6 +86,12 @@ public sealed partial class DirectExecutionBackend
|
||||
WriteCtxU64(contextRecord, 152, nextRsp);
|
||||
WriteCtxU64(contextRecord, 248, returnRip);
|
||||
Interlocked.Increment(ref _rawSentinelRecoveries);
|
||||
if (LogThreadMode)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"sentinel_recover rip=0x{value:X16} -> 0x{returnRip:X16} rsp=0x{rsp:X16} -> 0x{nextRsp:X16} " +
|
||||
$"gateway_depth={_threadModeGatewayDepth}");
|
||||
}
|
||||
return -1;
|
||||
}
|
||||
return 0;
|
||||
@@ -99,6 +122,12 @@ public sealed partial class DirectExecutionBackend
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
|
||||
_lastReportedRawSentinelRecoveries = num2;
|
||||
}
|
||||
// Leaf imports take a scalar-only fast path that reads its own operands and
|
||||
// intentionally bypasses the SysV variadic XMM marshalling below. This is safe
|
||||
// only while the leaf set contains no float-variadic or float-returning function.
|
||||
// The constraint and the audited list live on IsLeafImport — do not add a
|
||||
// function that consumes xmm0-7 args or returns in xmm0 to the leaf set;
|
||||
// such functions must stay on the full gateway path below.
|
||||
if (IsLeafImport(importStubEntry.Nid) &&
|
||||
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
|
||||
{
|
||||
@@ -118,10 +147,17 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
|
||||
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
||||
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
||||
cpuContext.SetXmmRegister(
|
||||
0,
|
||||
*(ulong*)(argPackPtr - 16),
|
||||
*(ulong*)(argPackPtr - 8));
|
||||
// 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;
|
||||
ulong value = cpuContext[CpuRegister.Rdi];
|
||||
ulong value2 = cpuContext[CpuRegister.Rsi];
|
||||
@@ -191,6 +227,17 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rax] = 1uL;
|
||||
return 1uL;
|
||||
}
|
||||
// Backend teardown in progress: unwind this worker to the host instead of
|
||||
// dispatching. RunGuestThread parks it as Blocked with no wake handler, its
|
||||
// host thread exits, and RequestGuestThreadTeardown can finish before any
|
||||
// executable memory is freed.
|
||||
if (isGuestWorker &&
|
||||
_guestTeardownRequested &&
|
||||
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "backend teardown"))
|
||||
{
|
||||
cpuContext[CpuRegister.Rax] = 0uL;
|
||||
return 0uL;
|
||||
}
|
||||
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
||||
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
||||
@@ -338,10 +385,15 @@ public sealed partial class DirectExecutionBackend
|
||||
{
|
||||
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();
|
||||
}
|
||||
else if (string.Equals(importStubEntry.Nid, "r8mvOaWdi28", StringComparison.Ordinal))
|
||||
{
|
||||
orbisGen2Result = DispatchIl2CppApiLookupSymbol();
|
||||
}
|
||||
else if (importStubEntry.Export is { } cachedExport &&
|
||||
(cachedExport.Target & cpuContext.TargetGeneration) != 0)
|
||||
{
|
||||
@@ -353,12 +405,21 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
orbisGen2Result = (OrbisGen2Result)returnValue;
|
||||
}
|
||||
else
|
||||
{
|
||||
dispatchResolved = false;
|
||||
orbisGen2Result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)(int)orbisGen2Result);
|
||||
}
|
||||
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
|
||||
{
|
||||
@@ -477,11 +538,20 @@ public sealed partial class DirectExecutionBackend
|
||||
Console.Error.Flush();
|
||||
}
|
||||
}
|
||||
// Publish the handler's XMM0 back into the argpack's xmm0 save slot; the
|
||||
// trampoline epilogue reloads it into the guest's XMM0, delivering float/double
|
||||
// return values (powf/logf/wcstod). Harmless for int/pointer returns (XMM is
|
||||
// volatile across a SysV call, so the guest never relies on a preserved XMM0).
|
||||
cpuContext.GetXmmRegister(0, out var returnXmm0Low, out var returnXmm0High);
|
||||
*(ulong*)(argPackPtr - 0x80) = returnXmm0Low;
|
||||
*(ulong*)(argPackPtr - 0x80 + 8) = returnXmm0High;
|
||||
return cpuContext[CpuRegister.Rax];
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
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;
|
||||
return 18446744071562199298uL;
|
||||
}
|
||||
@@ -527,15 +597,14 @@ public sealed partial class DirectExecutionBackend
|
||||
if (dispatchIndex % 100000 == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex}: {export.LibraryName}:{export.Name} ({importStubEntry.Nid})");
|
||||
$"[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}");
|
||||
}
|
||||
|
||||
var previousImportCallFrame = GuestThreadExecution.EnterImportCallFrame(
|
||||
returnRip,
|
||||
(ulong)argPackPtr + 104uL,
|
||||
ActiveGuestReturnSlotAddress);
|
||||
int returnValue;
|
||||
try
|
||||
if (IsNoBlockLeafImport(importStubEntry.Nid))
|
||||
{
|
||||
cpuContext.ClearRaxWriteFlag();
|
||||
returnValue = export.Function(cpuContext);
|
||||
@@ -544,9 +613,25 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
|
||||
}
|
||||
}
|
||||
finally
|
||||
else
|
||||
{
|
||||
GuestThreadExecution.RestoreImportCallFrame(previousImportCallFrame);
|
||||
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)
|
||||
@@ -591,20 +676,84 @@ public sealed partial class DirectExecutionBackend
|
||||
return true;
|
||||
}
|
||||
|
||||
// Subset of the leaf set that additionally skips the import-call-frame
|
||||
// bookkeeping. The same scalar-only (no XMM args, no XMM return) constraint
|
||||
// as IsLeafImport applies — see the note there before adding entries.
|
||||
// NOTE: this filter is only consulted after IsLeafImport accepts the NID;
|
||||
// entries listed here but not in IsLeafImport (scePadRead, scePadOpen,
|
||||
// sceUserServiceGetEvent, sceUserServiceGetPlatformPrivacySetting,
|
||||
// pthread_mutex_trylock) currently take the full gateway path.
|
||||
private static bool IsNoBlockLeafImport(string nid) =>
|
||||
nid is
|
||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||
"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
|
||||
"AV6ipCNa4Rw" or // strcasecmp
|
||||
"q1cHNfGycLI" or // scePadRead
|
||||
"xk0AcarP3V4" or // scePadOpen
|
||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
||||
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
|
||||
"K-jXhbt2gn4"; // pthread_mutex_trylock (scePthreadMutexTrylock is upoVrzMHFeE)
|
||||
|
||||
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
||||
{
|
||||
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;
|
||||
if (!expectedFileProbeMiss && !expectedTimedWaitTimeout)
|
||||
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;
|
||||
}
|
||||
|
||||
var key = nid + "\0" + (int)result;
|
||||
if (!ShouldLogExpectedImportResults())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var key = nid + "\0" + resultValue;
|
||||
int count;
|
||||
lock (_importResultLogSampleGate)
|
||||
{
|
||||
@@ -616,50 +765,112 @@ public sealed partial class DirectExecutionBackend
|
||||
return count <= 8 || count % 10000 == 0;
|
||||
}
|
||||
|
||||
private static bool ShouldLogExpectedImportResults() =>
|
||||
string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_EXPECTED_IMPORT_RESULTS"),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
|
||||
private static bool IsExpectedFileProbeNotFoundNid(string nid) =>
|
||||
nid is
|
||||
"eV9wAD2riIA" or // sceKernelStat
|
||||
"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")
|
||||
if (nid == "1jfXLRVzisc") // sceKernelUsleep
|
||||
{
|
||||
return !_logUsleep;
|
||||
}
|
||||
|
||||
return nid is
|
||||
"9UK1vLZQft4" or
|
||||
"tn3VlD0hG60" or
|
||||
"7H0iTOciTLo" or
|
||||
"2Z+PpY6CaJg" or
|
||||
"8aI7R7WaOlc" or
|
||||
"a8uLzYY--tM" or
|
||||
"Qs1xtplKo0U" or
|
||||
"GuchCTefuZw" or
|
||||
"N-FSPA4S3nI" or
|
||||
"baQO9ez2gL4" or
|
||||
"ULvXMDz56po" or
|
||||
"mQ16-QdKv7k" or
|
||||
"vWU-odnS+fU" or
|
||||
"sSAUCCU1dv4" or
|
||||
"C+IEj+BsAFM" or
|
||||
"tZDDEo2tE5k" or
|
||||
"GnxKOHEawhk" or
|
||||
"H896Pt-yB4I" or
|
||||
"sJXyWHjP-F8" or
|
||||
"ASoW5WE-UPo" or
|
||||
"rqwFKI4PAiM" or
|
||||
"eE4Szl8sil8" or
|
||||
"qvMUCyyaCSI" or
|
||||
"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
|
||||
"AV6ipCNa4Rw" or // strcasecmp
|
||||
"pNtJdE3x49E" or // wcscmp
|
||||
"fV2xHER+bKE" or // wcscoll
|
||||
"E8wCoUEbfzk" or // wcsncmp
|
||||
@@ -685,7 +896,12 @@ public sealed partial class DirectExecutionBackend
|
||||
"vz+pg2zdopI" or // sceKernelGetEventUserData
|
||||
"mJ7aghmgvfc" or // sceKernelGetEventId
|
||||
"23CPPI1tyBY" or // sceKernelGetEventFilter
|
||||
"kwGyyjohI50"; // sceKernelGetEventData
|
||||
"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()
|
||||
@@ -1102,8 +1318,11 @@ public sealed partial class DirectExecutionBackend
|
||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
||||
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;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -1116,6 +1335,62 @@ public sealed partial class DirectExecutionBackend
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address)
|
||||
{
|
||||
address = 0;
|
||||
var alias = symbolName switch
|
||||
{
|
||||
"scriptingGetMem" => "malloc",
|
||||
"scriptingFreeMem" => "free",
|
||||
"scriptingRealloc" => "realloc",
|
||||
"scriptingCalloc" => "calloc",
|
||||
_ => null,
|
||||
};
|
||||
|
||||
return alias != null && TryResolveRuntimeSymbolAddress(alias, out address);
|
||||
}
|
||||
|
||||
private OrbisGen2Result DispatchIl2CppApiLookupSymbol()
|
||||
{
|
||||
var cpuContext = ActiveCpuContext;
|
||||
if (cpuContext == null)
|
||||
{
|
||||
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var symbolNameAddress = cpuContext[CpuRegister.Rdi];
|
||||
var outputAddress = cpuContext[CpuRegister.Rsi];
|
||||
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
|
||||
outputAddress == 0 ||
|
||||
!TryResolveIl2CppApiAddress(symbolName, out var resolvedAddress) ||
|
||||
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][WARN] il2cpp_api_lookup_symbol failed: name='{symbolName}' out=0x{outputAddress:X16}");
|
||||
if (outputAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt64Compat(outputAddress, 0);
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
cpuContext[CpuRegister.Rax] = 0;
|
||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private bool TryResolveIl2CppApiAddress(string symbolName, out ulong address)
|
||||
{
|
||||
if (TryResolveRuntimeSymbolAddress(symbolName, out address))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
return Aerolib.Instance.TryGetByExportName(symbolName, out var symbol) &&
|
||||
TryResolveRuntimeSymbolAddress(symbol.Nid, out address);
|
||||
}
|
||||
|
||||
private OrbisGen2Result DispatchBootstrapBridge()
|
||||
{
|
||||
var cpuContext = ActiveCpuContext;
|
||||
|
||||
@@ -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();
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -10,11 +10,13 @@ using SharpEmu.Core;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Loader;
|
||||
|
||||
public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
||||
private const uint SelfMagic = 0x4F153D1D;
|
||||
private const ulong SelfSegmentFlag = 0x800;
|
||||
private const int PageSize = 0x1000;
|
||||
@@ -155,7 +157,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
}
|
||||
|
||||
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 elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
||||
@@ -255,7 +257,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
_nextTlsModuleId++;
|
||||
}
|
||||
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||
|
||||
return new SelfImage(
|
||||
loadContext.IsSelf,
|
||||
@@ -403,15 +405,15 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
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)
|
||||
{
|
||||
var vaddrMod = virtualAddress % header.Alignment;
|
||||
var offsetMod = header.Offset % header.Alignment;
|
||||
if (vaddrMod != offsetMod)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
|
||||
Log.Warning(
|
||||
$"Segment {index} ELF alignment mismatch! " +
|
||||
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
||||
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
|
||||
}
|
||||
@@ -671,13 +673,13 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||
Log.Debug(
|
||||
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,8 +691,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
||||
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
||||
Log.Debug(
|
||||
$"[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))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||
Log.Debug(
|
||||
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||
}
|
||||
|
||||
if (!IsSupportedRelocationType(relocation.Type))
|
||||
{
|
||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocation.Type}");
|
||||
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -800,7 +802,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
|
||||
Log.Debug("[FOCUS][SKIP] target address not mapped");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -840,7 +842,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -852,10 +854,9 @@ public sealed class SelfLoader : ISelfLoader
|
||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||
if (symbolAddress == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
Log.Warning(
|
||||
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||
continue;
|
||||
}
|
||||
|
||||
descriptors.Add(new RelocationDescriptor(
|
||||
@@ -873,10 +874,9 @@ public sealed class SelfLoader : ISelfLoader
|
||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||
if (symbolAddress == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
Log.Warning(
|
||||
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||
continue;
|
||||
}
|
||||
|
||||
descriptors.Add(new RelocationDescriptor(
|
||||
@@ -893,7 +893,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
|
||||
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -902,7 +902,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
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;
|
||||
}
|
||||
@@ -950,8 +950,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
||||
Log.Info(
|
||||
$"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)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
||||
Log.Info(
|
||||
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1409,8 +1409,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
|
||||
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||
Log.Debug(
|
||||
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||
if (mapSize > int.MaxValue)
|
||||
{
|
||||
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
|
||||
@@ -1842,16 +1842,16 @@ public sealed class SelfLoader : ISelfLoader
|
||||
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
|
||||
|
||||
var guestAddr = location + imageBase;
|
||||
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))
|
||||
{
|
||||
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;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1859,11 +1859,11 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
var slice = elfData.Slice((int)location, (int)size);
|
||||
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;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
|
||||
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
|
||||
tableBytes = Array.Empty<byte>();
|
||||
return false;
|
||||
}
|
||||
@@ -1920,17 +1920,17 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
|
||||
{
|
||||
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)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
|
||||
Log.Debug("-> return 0 (null)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -1939,18 +1939,18 @@ public sealed class SelfLoader : ISelfLoader
|
||||
var rebased = address + imageBase;
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
if (address < 0x10000)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
|
||||
Log.Debug($"-> reject small 0x{address:X}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
|
||||
Log.Debug($"-> fallback raw 0x{address:X}");
|
||||
return address;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,19 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Memory;
|
||||
|
||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
||||
{
|
||||
private 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 _allocationSearchHintGate = new();
|
||||
private readonly List<MemoryRegion> _regions = new();
|
||||
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||
private bool _disposed;
|
||||
private const ulong PageSize = 0x1000;
|
||||
@@ -19,7 +24,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
||||
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
||||
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
||||
private const ulong 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 uint MEM_COMMIT = 0x1000;
|
||||
@@ -35,6 +41,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private ulong _guestAllocationArenaBase;
|
||||
private ulong _guestAllocationOffset;
|
||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||
@@ -51,7 +58,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern void 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)
|
||||
{
|
||||
@@ -78,9 +89,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return false;
|
||||
}
|
||||
|
||||
lock (_gate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
@@ -89,9 +101,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
Protection = protection
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -105,7 +121,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
||||
var reservedOnly = false;
|
||||
var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold;
|
||||
var preferReserveOnly = !executable &&
|
||||
alignedSize >= LargeDataReserveThreshold &&
|
||||
alignedSize > FullCommitRegionLimit;
|
||||
|
||||
void* result = null;
|
||||
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)");
|
||||
}
|
||||
|
||||
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);
|
||||
|
||||
if (result == null)
|
||||
@@ -188,12 +206,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
lazyPrimeState = committedBytes == primeBytes
|
||||
? $"ok:{committedBytes: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
|
||||
{
|
||||
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
|
||||
@@ -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,
|
||||
Size = alignedSize,
|
||||
@@ -213,11 +232,15 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
Protection = protection
|
||||
});
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
var allocationKind = reservedOnly
|
||||
? "reserved data memory (lazy commit)"
|
||||
: (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;
|
||||
}
|
||||
@@ -237,7 +260,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
var alignedSize = AlignUp(size, PageSize);
|
||||
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++)
|
||||
{
|
||||
@@ -257,6 +281,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
||||
if (actualAddress == cursor)
|
||||
{
|
||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -315,7 +340,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
{
|
||||
lock (_guestAllocationGate)
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
@@ -323,6 +349,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
_regions.Clear();
|
||||
_pageProtections.Clear();
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
_allocationSearchHints.Clear();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
|
||||
_guestAllocationArenaBase = 0;
|
||||
@@ -343,7 +377,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var mapEnd = AlignUp(segmentEnd, PageSize);
|
||||
var mapSize = checked(mapEnd - mapStart);
|
||||
|
||||
lock (_gate)
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
var existingRegion = FindRegion(mapStart, mapSize);
|
||||
if (existingRegion == null)
|
||||
@@ -374,7 +409,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
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)
|
||||
{
|
||||
FlushInstructionCache(null, (void*)address, (nuint)size);
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||
}
|
||||
}
|
||||
|
||||
public IReadOnlyList<VirtualMemoryRegion> SnapshotRegions()
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var snapshot = new VirtualMemoryRegion[_regions.Count];
|
||||
for (var i = 0; i < _regions.Count; i++)
|
||||
@@ -440,116 +480,240 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
return snapshot;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||
}
|
||||
|
||||
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);
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
|
||||
if (!requiresExclusiveAccess)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
return TryReadExclusive(virtualAddress, destination);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitWriteLock();
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
if (source.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (byte* srcPtr = source)
|
||||
{
|
||||
fixed (byte* srcPtr = source)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
}
|
||||
|
||||
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(null, destPtr, (nuint)source.Length);
|
||||
}
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
|
||||
if (!requiresExclusiveAccess)
|
||||
{
|
||||
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)
|
||||
@@ -561,45 +725,76 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
public void* GetPointer(ulong virtualAddress)
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (virtualAddress >= region.VirtualAddress &&
|
||||
virtualAddress < region.VirtualAddress + region.Size)
|
||||
{
|
||||
return (void*)virtualAddress;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return FindRegion(virtualAddress, 1) is not null
|
||||
? (void*)virtualAddress
|
||||
: null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool IsAccessible(ulong virtualAddress, ulong size)
|
||||
{
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (TryResolveRegionOffset(virtualAddress, size, region, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return FindRegion(virtualAddress, size) is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -611,21 +806,67 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
|
||||
var end = address + size;
|
||||
lock (_gate)
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
var regionEnd = region.VirtualAddress + region.Size;
|
||||
if (region.VirtualAddress >= end)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (regionEnd <= address)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address < regionEnd && region.VirtualAddress < end)
|
||||
{
|
||||
overlapEnd = Math.Max(overlapEnd, regionEnd);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
|
||||
return overlapEnd != 0;
|
||||
}
|
||||
|
||||
private ulong GetAllocationSearchCursor(
|
||||
ulong desiredAddress,
|
||||
ulong requestedCursor,
|
||||
ulong alignment,
|
||||
bool executable)
|
||||
{
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
var key = (desiredAddress, alignment, executable);
|
||||
if (_allocationSearchHints.TryGetValue(key, out var hintedCursor) &&
|
||||
hintedCursor > requestedCursor)
|
||||
{
|
||||
return AlignUp(hintedCursor, alignment);
|
||||
}
|
||||
}
|
||||
|
||||
return requestedCursor;
|
||||
}
|
||||
|
||||
private void UpdateAllocationSearchCursor(
|
||||
ulong desiredAddress,
|
||||
ulong alignment,
|
||||
bool executable,
|
||||
ulong nextCursor)
|
||||
{
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
_allocationSearchHints[(desiredAddress, alignment, executable)] = AlignUp(nextCursor, alignment);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
|
||||
{
|
||||
offset = 0;
|
||||
@@ -707,15 +948,26 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var endPage = AlignUp(address + size, PageSize);
|
||||
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)
|
||||
{
|
||||
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)
|
||||
{
|
||||
pageAddress = rangeEnd;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -724,10 +976,13 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
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;
|
||||
}
|
||||
|
||||
pageAddress = rangeEnd;
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -780,6 +1035,29 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return checked((value + mask) & ~mask);
|
||||
}
|
||||
|
||||
private static ulong ResolveLazyReservePrimeBytes()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_LAZY_RESERVE_PRIME_MB");
|
||||
if (ulong.TryParse(configured, out var megabytes))
|
||||
{
|
||||
return megabytes == 0
|
||||
? 0
|
||||
: checked(Math.Min(megabytes, 4096UL) * 1024UL * 1024UL);
|
||||
}
|
||||
|
||||
return DefaultLazyReservePrimeBytes;
|
||||
}
|
||||
|
||||
private static void TraceVmem(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Debug(message);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
|
||||
@@ -10,15 +10,20 @@ using SharpEmu.HLE;
|
||||
using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.AppContent;
|
||||
using SharpEmu.Libs.SaveData;
|
||||
using SharpEmu.Logging;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
|
||||
namespace SharpEmu.Core.Runtime;
|
||||
|
||||
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
|
||||
|
||||
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
||||
|
||||
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -127,7 +132,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||
Log.Info($"Loading: {ebootPath}");
|
||||
LastExecutionDiagnostics = null;
|
||||
LastExecutionTrace = null;
|
||||
LastSessionSummary = null;
|
||||
@@ -135,10 +140,12 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
LastMilestoneLog = null;
|
||||
KernelModuleRegistry.Reset();
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
|
||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||
LogAppBundleInfo(normalizedEbootPath, image);
|
||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
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 activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
||||
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
||||
@@ -161,7 +168,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName);
|
||||
if (initializerResult is { } failedInitializerResult)
|
||||
{
|
||||
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
||||
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -169,8 +176,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return failedInitializerResult;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
||||
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
Log.Info($"Dispatching, gen: {generation}");
|
||||
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
image.EntryPoint,
|
||||
@@ -180,8 +187,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName,
|
||||
_cpuExecutionOptions);
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||
Log.Info($"DispatchEntry returned: {result}");
|
||||
Log.Info($"Dispatch result: {result}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -352,6 +359,66 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
|
||||
{
|
||||
var executableName = Path.GetFileName(ebootPath);
|
||||
if (string.IsNullOrWhiteSpace(executableName))
|
||||
{
|
||||
executableName = "eboot.bin";
|
||||
}
|
||||
|
||||
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
|
||||
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
|
||||
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
|
||||
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("App bundle info:");
|
||||
builder.AppendLine($"- Display name: {displayName}");
|
||||
builder.AppendLine($"- Version: {version}");
|
||||
builder.AppendLine($"- Title ID: {titleId}");
|
||||
builder.AppendLine($"- Content ID: {contentId}");
|
||||
builder.AppendLine($"- Executable: {executableName}");
|
||||
builder.Append("- Platform: PlayStation 5");
|
||||
Log.Info(builder.ToString());
|
||||
}
|
||||
|
||||
private static string? ResolveContentId(string? bundleRoot)
|
||||
{
|
||||
if (string.IsNullOrEmpty(bundleRoot))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var candidate in new[]
|
||||
{
|
||||
Path.Combine(bundleRoot, "sce_sys", "param.json"),
|
||||
Path.Combine(bundleRoot, "param.json"),
|
||||
})
|
||||
{
|
||||
if (!File.Exists(candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
|
||||
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
|
||||
{
|
||||
return contentId.GetString();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
||||
{
|
||||
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
||||
@@ -432,8 +499,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
moduleName = $"module#{i}";
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
Log.Info(
|
||||
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||
initEntryPoint,
|
||||
@@ -444,8 +511,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
_cpuExecutionOptions);
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
||||
Log.Error(
|
||||
$"Module start failed: {moduleName} -> {result}");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -466,8 +533,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return null;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
Log.Info(
|
||||
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
|
||||
var result = RunInitializerList(
|
||||
$"{label}:preinit",
|
||||
@@ -506,8 +573,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
Log.Debug(
|
||||
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
initializerAddress,
|
||||
@@ -541,6 +608,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
Path.Combine(ebootDirectory, "sce_module"),
|
||||
Path.Combine(ebootDirectory, "sce_modules"),
|
||||
Path.Combine(ebootDirectory, "Media", "Modules"),
|
||||
}
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Where(Directory.Exists)
|
||||
@@ -552,7 +620,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
}
|
||||
|
||||
var allModulePaths = moduleDirectories
|
||||
.SelectMany(Directory.EnumerateFiles)
|
||||
.SelectMany(directory => Directory
|
||||
.EnumerateFiles(directory)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
||||
.Where(path =>
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
@@ -560,7 +630,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
||||
})
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var modulePaths = allModulePaths
|
||||
@@ -573,7 +642,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
.ToArray();
|
||||
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)
|
||||
@@ -581,8 +650,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
||||
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
||||
var loadedModules = 0;
|
||||
var failedModules = 0;
|
||||
var mergedImportCount = 0;
|
||||
@@ -617,18 +686,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
||||
loadedModules++;
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
Log.Info(
|
||||
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
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(
|
||||
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
Log.Info(
|
||||
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
@@ -648,8 +717,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (rebound != 0 || unresolved != 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
Log.Info(
|
||||
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -681,8 +750,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
Log.Warning(
|
||||
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -694,8 +763,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Log.Error(
|
||||
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -704,8 +773,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Log.Debug(
|
||||
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
rebound++;
|
||||
@@ -741,8 +810,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
Log.Warning(
|
||||
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -851,8 +920,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
image.EntryPoint,
|
||||
isMain,
|
||||
isSystemModule);
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||
Log.Info(
|
||||
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||
}
|
||||
|
||||
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
@@ -14,6 +14,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageReference Include="Iced" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -65,13 +65,17 @@
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
@@ -88,6 +92,16 @@
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
|
||||
@@ -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; }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Linq;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public sealed class Aerolib : ISymbolCatalog
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Aerolib");
|
||||
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
|
||||
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
|
||||
|
||||
@@ -108,14 +110,14 @@ public sealed class Aerolib : ISymbolCatalog
|
||||
|
||||
if (resourceName == null)
|
||||
{
|
||||
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
|
||||
Log.Error("Embedded resource 'aerolib.bin' not found");
|
||||
return;
|
||||
}
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null)
|
||||
{
|
||||
Console.Error.WriteLine("[AEROLIB] Failed to open embedded resource stream");
|
||||
Log.Error("Failed to open embedded resource stream");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,11 +147,11 @@ public sealed class Aerolib : ISymbolCatalog
|
||||
_byExportName[name] = symbol;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[AEROLIB] Loaded {_byNid.Count} NID entries from binary resource");
|
||||
Log.Info($"Loaded {_byNid.Count} NID entries from binary resource");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[AEROLIB] Failed to load embedded aerolib.bin: {ex.Message}");
|
||||
Log.Error($"Failed to load embedded aerolib.bin: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -2,25 +2,20 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
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[] _xmmRegisters = new ulong[32];
|
||||
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
|
||||
private bool _raxWritten;
|
||||
|
||||
public CpuContext(ICpuMemory memory, Generation generation)
|
||||
{
|
||||
Memory = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||
TargetGeneration = generation;
|
||||
}
|
||||
public ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||
|
||||
public ICpuMemory Memory { get; }
|
||||
|
||||
public Generation TargetGeneration { get; }
|
||||
public Generation TargetGeneration { get; } = generation;
|
||||
|
||||
public ulong Rip { get; set; }
|
||||
|
||||
@@ -130,6 +125,91 @@ public sealed class CpuContext
|
||||
SetYmmUpper(registerIndex, highLow, highHigh);
|
||||
}
|
||||
|
||||
public bool TryReadByte(ulong address, out byte value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
if (!Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = buffer[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryReadUInt16(ulong address, out ushort value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||
if (!Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteUInt16(ulong address, ushort value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadInt32(ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteInt32(ulong address, int value, bool checkNil = false)
|
||||
{
|
||||
if (checkNil && address == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
public bool TryReadUInt32(ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteUInt32(ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryWriteInt64(ulong address, long value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(long)];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadUInt64(ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
@@ -150,6 +230,33 @@ public sealed class CpuContext
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadNullTerminatedUtf8(ulong address, int capacity, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (address == 0 || capacity <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytes[index] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool PushUInt64(ulong value)
|
||||
{
|
||||
var rsp = this[CpuRegister.Rsp];
|
||||
@@ -169,4 +276,23 @@ public sealed class CpuContext
|
||||
this[CpuRegister.Rsp] = rsp + sizeof(ulong);
|
||||
return true;
|
||||
}
|
||||
|
||||
public int SetReturn(int result, Type? cast = null)
|
||||
{
|
||||
var value = cast switch
|
||||
{
|
||||
null => (ulong)result,
|
||||
_ when cast == typeof(long) => (ulong)(long)result,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
this[CpuRegister.Rax] = unchecked(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
public int SetReturn(OrbisGen2Result result)
|
||||
{
|
||||
this[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -10,7 +10,9 @@ public readonly record struct GuestThreadStartRequest(
|
||||
ulong EntryPoint,
|
||||
ulong Argument,
|
||||
ulong AttributeAddress,
|
||||
string Name);
|
||||
string Name,
|
||||
int Priority,
|
||||
ulong AffinityMask);
|
||||
|
||||
public readonly record struct GuestThreadSnapshot(
|
||||
ulong ThreadHandle,
|
||||
@@ -27,6 +29,12 @@ public interface IGuestThreadScheduler
|
||||
|
||||
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);
|
||||
|
||||
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
|
||||
|
||||
@@ -3,11 +3,14 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public sealed class ModuleManager : IModuleManager
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("HLE");
|
||||
|
||||
private readonly ConcurrentDictionary<string, Delegate> _dispatchTable = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||
@@ -47,6 +50,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
var handler = CreateHandler(type, method, instances);
|
||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||
{
|
||||
Log.Warning($"Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -105,6 +109,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||
{
|
||||
Log.Warning($"NID '{nid}' not found in dispatch table.");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return false;
|
||||
@@ -112,11 +117,13 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if ((export.Target & context.TargetGeneration) == 0)
|
||||
{
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
Log.Warning($"NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
context.ClearRaxWriteFlag();
|
||||
int ret = ((SysAbiFunction)function).Invoke(context);
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ public enum OrbisGen2Result : int
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_DEADLOCK = unchecked((int)0x8002000B),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the waited-on object was deleted while the caller was
|
||||
/// blocked on it. Matches the SCE kernel EACCES code that waiters of a
|
||||
/// deleted semaphore observe.
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_DELETED = unchecked((int)0x8002000D),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the target resource is busy.
|
||||
/// </summary>
|
||||
@@ -50,6 +57,13 @@ public enum OrbisGen2Result : int
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_TRY_AGAIN = unchecked((int)0x80020023),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a blocked wait was canceled (e.g. sceKernelCancelSema).
|
||||
/// Matches the SCE kernel ECANCELED code that canceled semaphore waiters
|
||||
/// observe.
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_CANCELED = unchecked((int)0x80020055),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that behavior is recognized but not implemented yet.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,6 +8,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {}
|
||||
"net10.0": {
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+4230
-260
File diff suppressed because it is too large
Load Diff
@@ -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;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -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.Libs.Kernel;
|
||||
using System.Buffers;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
|
||||
@@ -23,8 +24,12 @@ public static class AmprExports
|
||||
private const uint KernelEventQueueRecordType = 2;
|
||||
private const uint WriteAddressRecordType = 3;
|
||||
private static readonly ConcurrentDictionary<ulong, CommandBufferState> _commandBuffers = new();
|
||||
private static readonly ConcurrentDictionary<string, Lazy<CachedHostFile>> _hostFileCache = new(StringComparer.OrdinalIgnoreCase);
|
||||
private static readonly bool _traceAmpr =
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR"), "1", StringComparison.Ordinal);
|
||||
private static readonly bool _traceAmprReads =
|
||||
_traceAmpr ||
|
||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AMPR_READS"), "1", StringComparison.Ordinal);
|
||||
|
||||
private sealed class CommandBufferState
|
||||
{
|
||||
@@ -33,6 +38,23 @@ public static class AmprExports
|
||||
public ulong WriteOffset;
|
||||
}
|
||||
|
||||
private sealed class CachedHostFile
|
||||
{
|
||||
public CachedHostFile(string path)
|
||||
{
|
||||
Stream = new FileStream(
|
||||
path,
|
||||
FileMode.Open,
|
||||
FileAccess.Read,
|
||||
FileShare.ReadWrite | FileShare.Delete,
|
||||
bufferSize: 1024 * 1024,
|
||||
FileOptions.RandomAccess);
|
||||
}
|
||||
|
||||
public object Gate { get; } = new();
|
||||
public FileStream Stream { get; }
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "8aI7R7WaOlc",
|
||||
ExportName = "sceAmprCommandBufferConstructor",
|
||||
@@ -77,12 +99,7 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
var buffer = 0UL;
|
||||
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))
|
||||
if (!InitializeCommandBuffer(ctx, commandBuffer, buffer: 0, size: 0, aux0, aux1, clear: false))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -106,8 +123,9 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, 0) ||
|
||||
!ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, 0))
|
||||
Span<byte> auxiliaryPointers = stackalloc byte[sizeof(ulong) * 2];
|
||||
auxiliaryPointers.Clear();
|
||||
if (!ctx.Memory.TryWrite(commandBuffer + CommandBufferAux0Offset, auxiliaryPointers))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -181,8 +199,15 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out var buffer) ||
|
||||
!ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out var size) ||
|
||||
Span<byte> bufferPointers = stackalloc byte[sizeof(ulong) * 2];
|
||||
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))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -242,19 +267,53 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
ulong bytesRead = 0;
|
||||
|
||||
// Unregistered/missing files are zero-filled instead of failing: games queue
|
||||
// speculative reads and only consume the bytes on success paths.
|
||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath) || !File.Exists(hostPath))
|
||||
{
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead: 0, hostPath, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
if (destination != 0 && size > 0)
|
||||
{
|
||||
int chunkSize = (int)Math.Min(size, 4096);
|
||||
Span<byte> zeros = stackalloc byte[chunkSize];
|
||||
zeros.Clear();
|
||||
while (bytesRead < size)
|
||||
{
|
||||
int currentChunk = (int)Math.Min((ulong)chunkSize, size - bytesRead);
|
||||
if (!ctx.Memory.TryWrite(destination + bytesRead, zeros[..currentChunk]))
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
bytesRead += (ulong)currentChunk;
|
||||
}
|
||||
}
|
||||
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, "(missing)", (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
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)
|
||||
{
|
||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
||||
return result;
|
||||
}
|
||||
|
||||
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
|
||||
|
||||
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
@@ -428,7 +487,7 @@ public static class AmprExports
|
||||
var offset = 0UL;
|
||||
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;
|
||||
}
|
||||
@@ -476,20 +535,34 @@ public static class AmprExports
|
||||
ulong aux1,
|
||||
bool clear)
|
||||
{
|
||||
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
|
||||
if (clear)
|
||||
{
|
||||
Span<byte> header = stackalloc byte[CommandBufferHeaderSize];
|
||||
header.Clear();
|
||||
if (!ctx.Memory.TryWrite(commandBuffer, header))
|
||||
}
|
||||
else
|
||||
{
|
||||
if (!ctx.Memory.TryRead(commandBuffer, header))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
buffer = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferDataOffset..]);
|
||||
size = BinaryPrimitives.ReadUInt64LittleEndian(header[(int)CommandBufferSizeOffset..]);
|
||||
}
|
||||
|
||||
return ctx.TryWriteUInt64(commandBuffer + CommandBufferSelfOffset, commandBuffer) &&
|
||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux0Offset, aux0) &&
|
||||
ctx.TryWriteUInt64(commandBuffer + CommandBufferAux1Offset, aux1) &&
|
||||
WriteCommandBufferPointers(ctx, commandBuffer, buffer, size, writeOffset: 0);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSelfOffset..], commandBuffer);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferDataOffset..], buffer);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(header[(int)CommandBufferSizeOffset..], size);
|
||||
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)
|
||||
@@ -504,6 +577,26 @@ public static class AmprExports
|
||||
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());
|
||||
lock (state)
|
||||
{
|
||||
@@ -511,15 +604,6 @@ public static class AmprExports
|
||||
state.Size = size;
|
||||
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(
|
||||
@@ -540,9 +624,11 @@ public static class AmprExports
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ctx.TryReadUInt64(commandBuffer + CommandBufferDataOffset, out buffer) &&
|
||||
ctx.TryReadUInt64(commandBuffer + CommandBufferSizeOffset, out size))
|
||||
Span<byte> pointers = stackalloc byte[sizeof(ulong) * 2];
|
||||
if (ctx.Memory.TryRead(commandBuffer + CommandBufferDataOffset, pointers))
|
||||
{
|
||||
buffer = BinaryPrimitives.ReadUInt64LittleEndian(pointers);
|
||||
size = BinaryPrimitives.ReadUInt64LittleEndian(pointers[sizeof(ulong)..]);
|
||||
state = _commandBuffers.GetOrAdd(commandBuffer, static _ => new CommandBufferState());
|
||||
lock (state)
|
||||
{
|
||||
@@ -595,23 +681,48 @@ public static class AmprExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
const int ChunkSize = 64 * 1024;
|
||||
var buffer = new byte[(int)Math.Min((ulong)ChunkSize, size)];
|
||||
const int ChunkSize = 1024 * 1024;
|
||||
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ChunkSize, size));
|
||||
|
||||
try
|
||||
{
|
||||
using var stream = new FileStream(hostPath, FileMode.Open, FileAccess.Read, FileShare.ReadWrite | FileShare.Delete);
|
||||
if (fileOffset >= (ulong)stream.Length)
|
||||
if (!TryGetCachedHostFile(hostPath, out var cachedFile, out var openResult))
|
||||
{
|
||||
return openResult;
|
||||
}
|
||||
|
||||
long fileLength;
|
||||
lock (cachedFile.Gate)
|
||||
{
|
||||
fileLength = cachedFile.Stream.Length;
|
||||
}
|
||||
|
||||
if (fileOffset >= (ulong)fileLength)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
stream.Position = unchecked((long)fileOffset);
|
||||
|
||||
while (bytesRead < size)
|
||||
{
|
||||
if (bytesRead > ulong.MaxValue - fileOffset)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var absoluteOffset = fileOffset + bytesRead;
|
||||
if (absoluteOffset > long.MaxValue)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
|
||||
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)
|
||||
{
|
||||
break;
|
||||
@@ -633,10 +744,52 @@ public static class AmprExports
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
finally
|
||||
{
|
||||
ArrayPool<byte>.Shared.Return(buffer);
|
||||
}
|
||||
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
private static bool TryGetCachedHostFile(string hostPath, out CachedHostFile file, out int result)
|
||||
{
|
||||
file = null!;
|
||||
result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
|
||||
string cachePath;
|
||||
try
|
||||
{
|
||||
cachePath = Path.GetFullPath(hostPath);
|
||||
}
|
||||
catch
|
||||
{
|
||||
cachePath = hostPath;
|
||||
}
|
||||
|
||||
var lazy = _hostFileCache.GetOrAdd(
|
||||
cachePath,
|
||||
static path => new Lazy<CachedHostFile>(() => new CachedHostFile(path), isThreadSafe: true));
|
||||
|
||||
try
|
||||
{
|
||||
file = lazy.Value;
|
||||
return true;
|
||||
}
|
||||
catch (UnauthorizedAccessException)
|
||||
{
|
||||
_hostFileCache.TryRemove(cachePath, out _);
|
||||
result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||
return false;
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
_hostFileCache.TryRemove(cachePath, out _);
|
||||
result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool AppendReadFileRecord(
|
||||
CpuContext ctx,
|
||||
ulong commandBuffer,
|
||||
@@ -764,19 +917,6 @@ public static class AmprExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static void TraceAmpr(CpuContext ctx, string operation, ulong commandBuffer, ulong arg0, ulong arg1)
|
||||
{
|
||||
if (!_traceAmpr)
|
||||
@@ -801,7 +941,7 @@ public static class AmprExports
|
||||
string? hostPath,
|
||||
int result)
|
||||
{
|
||||
if (!_traceAmpr)
|
||||
if (!_traceAmprReads)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ internal static class AmprFileRegistry
|
||||
return _hostPathsById.TryGetValue(id, out hostPath!);
|
||||
}
|
||||
|
||||
private static uint ComputeFileId(string guestPath)
|
||||
internal static uint ComputeFileId(string guestPath)
|
||||
{
|
||||
var bytes = System.Text.Encoding.UTF8.GetBytes(guestPath);
|
||||
|
||||
|
||||
@@ -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];
|
||||
if (valueAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
int value;
|
||||
@@ -83,18 +83,18 @@ public static class AppContentExports
|
||||
}
|
||||
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)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(valueBytes, value);
|
||||
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}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[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)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_APP_CONTENT"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -3,15 +3,19 @@
|
||||
|
||||
using SharpEmu.HLE;
|
||||
using System.Buffers.Binary;
|
||||
using System.Threading;
|
||||
|
||||
namespace SharpEmu.Libs.Audio;
|
||||
|
||||
public static class AudioOut2Exports
|
||||
{
|
||||
private const int AudioOut2ContextParamSize = 0x80;
|
||||
// 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 const int AudioOut2ContextMemoryAlignment = 0x10000;
|
||||
private static long _nextContextHandle = 1;
|
||||
private static long _nextUserHandle = 1;
|
||||
private static int _nextPortId;
|
||||
@@ -37,7 +41,7 @@ public static class AudioOut2Exports
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
if (paramAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> param = stackalloc byte[AudioOut2ContextParamSize];
|
||||
@@ -48,8 +52,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(param[0x0C..], 0x400);
|
||||
|
||||
return ctx.Memory.TryWrite(paramAddress, param)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -60,22 +64,15 @@ public static class AudioOut2Exports
|
||||
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||
{
|
||||
var paramAddress = ctx[CpuRegister.Rdi];
|
||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
||||
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
|
||||
if (paramAddress == 0 || outMemorySizeAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> memoryInfo = stackalloc byte[0x20];
|
||||
memoryInfo.Clear();
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x00..], AudioOut2ContextMemorySize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x08..], AudioOut2ContextMemoryAlignment);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x10..], AudioOut2ContextMemorySize);
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(memoryInfo[0x18..], AudioOut2ContextMemoryAlignment);
|
||||
|
||||
return ctx.Memory.TryWrite(memoryInfoAddress, memoryInfo)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
|
||||
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -91,13 +88,13 @@ public static class AudioOut2Exports
|
||||
var outContextAddress = ctx[CpuRegister.Rcx];
|
||||
if (paramAddress == 0 || memoryAddress == 0 || memorySize == 0 || outContextAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextContextHandle);
|
||||
return TryWriteUInt64(ctx, outContextAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.TryWriteUInt64(outContextAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -105,7 +102,7 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2ContextDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
public static int AudioOut2ContextDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "JK2wamZPzwM",
|
||||
@@ -120,14 +117,14 @@ public static class AudioOut2Exports
|
||||
var contextAddress = ctx[CpuRegister.Rcx];
|
||||
if (type < 0 || type > 255 || paramAddress == 0 || outPortAddress == 0 || contextAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
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 TryWriteUInt64(ctx, outPortAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
return ctx.TryWriteUInt64(outPortAddress, handle)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -141,7 +138,7 @@ public static class AudioOut2Exports
|
||||
var stateAddress = ctx[CpuRegister.Rsi];
|
||||
if (handle == 0 || stateAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var type = (int)((handle >> 16) & 0xFF);
|
||||
@@ -154,8 +151,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteInt16LittleEndian(state[0x04..], -1);
|
||||
|
||||
return ctx.Memory.TryWrite(stateAddress, state)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -168,7 +165,7 @@ public static class AudioOut2Exports
|
||||
var infoAddress = ctx[CpuRegister.Rdi];
|
||||
if (infoAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
Span<byte> info = stackalloc byte[0x40];
|
||||
@@ -178,8 +175,8 @@ public static class AudioOut2Exports
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(info[0x08..], 48000);
|
||||
|
||||
return ctx.Memory.TryWrite(infoAddress, info)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -187,14 +184,14 @@ public static class AudioOut2Exports
|
||||
ExportName = "sceAudioOut2PortDestroy",
|
||||
Target = Generation.Gen5,
|
||||
LibraryName = "libSceAudioOut2")]
|
||||
public static int AudioOut2PortDestroy(CpuContext ctx) => SetReturn(ctx, 0);
|
||||
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) => SetReturn(ctx, 0);
|
||||
public static int AudioOut2UserDestroy(CpuContext ctx) => ctx.SetReturn(0);
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "xywYcRB7nbQ",
|
||||
@@ -207,25 +204,12 @@ public static class AudioOut2Exports
|
||||
var outUserAddress = ctx[CpuRegister.Rsi];
|
||||
if ((userId != 0 && userId != 1 && userId != 255) || outUserAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
var handle = (ulong)Interlocked.Increment(ref _nextUserHandle);
|
||||
return TryWriteUInt64(ctx, outUserAddress, handle)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
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
|
||||
{
|
||||
private const int AlreadySystemInitialized = unchecked((int)0x80B80002);
|
||||
private static int _initialized;
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -18,11 +17,12 @@ public static class CommonDialogExports
|
||||
LibraryName = "libSceCommonDialog")]
|
||||
public static int CommonDialogInitialize(CpuContext ctx)
|
||||
{
|
||||
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
||||
? 0
|
||||
: AlreadySystemInitialized;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
// Games can initialize the common-dialog service defensively from more than one
|
||||
// subsystem. Keeping this HLE initialization idempotent avoids turning an error
|
||||
// reporting path into a second, unrelated failure.
|
||||
Interlocked.Exchange(ref _initialized, 1);
|
||||
ctx[CpuRegister.Rax] = 0;
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
[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(
|
||||
Nid = "",
|
||||
Nid = "9rAeANT2tyE",
|
||||
ExportName = "__cxa_guard_release",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libc")]
|
||||
@@ -142,7 +142,7 @@ public static class CxaGuardExports
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
Nid = "",
|
||||
Nid = "2emaaluWzUw",
|
||||
ExportName = "__cxa_guard_abort",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
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}");
|
||||
}
|
||||
}
|
||||
@@ -104,17 +104,17 @@ public static class FiberExports
|
||||
var optParam = ctx[CpuRegister.Rdi];
|
||||
if (optParam == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((optParam & 7) != 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorAlignment);
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
}
|
||||
|
||||
return TryWriteUInt32(ctx, optParam, FiberOptSignature)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.TryWriteUInt32(optParam, FiberOptSignature)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -127,24 +127,24 @@ public static class FiberExports
|
||||
var fiber = ctx[CpuRegister.Rdi];
|
||||
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)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations.TryRemove(fiber, out _);
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
_stackRanges.TryRemove(fiber, out _);
|
||||
_ = TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateTerminated);
|
||||
return SetReturn(ctx, 0);
|
||||
_ = ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateTerminated);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -229,20 +229,20 @@ public static class FiberExports
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
var returnArgument = ctx[CpuRegister.Rdi];
|
||||
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);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
@@ -256,7 +256,7 @@ public static class FiberExports
|
||||
if (!_returnTargets.TryRemove(fiberAddress, out var returnTarget))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
previousFiber = returnTarget.PreviousFiber;
|
||||
@@ -264,10 +264,10 @@ public static class FiberExports
|
||||
{
|
||||
if (!_continuations.TryRemove(previousFiber, out var previousContinuation) ||
|
||||
!TryWriteResumeArgument(ctx, previousContinuation, returnArgument) ||
|
||||
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun))
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = previousContinuation.Context with { Rax = 0 };
|
||||
@@ -278,15 +278,15 @@ public static class FiberExports
|
||||
!TryWriteResumeArgument(ctx, returnTarget.ThreadContinuation.Value, returnArgument))
|
||||
{
|
||||
_continuations.TryRemove(fiberAddress, out _);
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
transferTarget = returnTarget.ThreadContinuation.Value.Context with { Rax = 0 };
|
||||
}
|
||||
|
||||
if (!TryWriteUInt32(ctx, fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
if (!ctx.TryWriteUInt32(fiberAddress + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -296,7 +296,7 @@ public static class FiberExports
|
||||
TraceFiber(
|
||||
$"return fiber=0x{fiberAddress:X16} to=0x{previousFiber:X16} " +
|
||||
$"resume=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{returnArgument:X16}");
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -309,18 +309,18 @@ public static class FiberExports
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
var fiberAddress = ResolveCurrentFiberAddress(ctx);
|
||||
if (fiberAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
return TryWriteUInt64(ctx, outAddress, fiberAddress)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.TryWriteUInt64(outAddress, fiberAddress)
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -334,35 +334,35 @@ public static class FiberExports
|
||||
var info = ctx[CpuRegister.Rsi];
|
||||
if (info == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (!TryWriteUInt64(ctx, info + 8, fields.Entry) ||
|
||||
!TryWriteUInt64(ctx, info + 16, fields.ArgOnInitialize) ||
|
||||
!TryWriteUInt64(ctx, info + 24, fields.ContextAddress) ||
|
||||
!TryWriteUInt64(ctx, info + 32, fields.ContextSize) ||
|
||||
if (!ctx.TryWriteUInt64(info + 8, fields.Entry) ||
|
||||
!ctx.TryWriteUInt64(info + 16, fields.ArgOnInitialize) ||
|
||||
!ctx.TryWriteUInt64(info + 24, fields.ContextAddress) ||
|
||||
!ctx.TryWriteUInt64(info + 32, fields.ContextSize) ||
|
||||
!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(
|
||||
@@ -376,22 +376,22 @@ public static class FiberExports
|
||||
var nameAddress = ctx[CpuRegister.Rsi];
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return SetReturn(ctx, error);
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
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)
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -403,12 +403,12 @@ public static class FiberExports
|
||||
{
|
||||
if (ctx[CpuRegister.Rdi] != 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 1) == 0
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorState);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -419,8 +419,8 @@ public static class FiberExports
|
||||
public static int FiberStopContextSizeCheck(CpuContext ctx)
|
||||
{
|
||||
return Interlocked.Exchange(ref _contextSizeCheck, 0) == 1
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorState);
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -433,17 +433,17 @@ public static class FiberExports
|
||||
var outAddress = ctx[CpuRegister.Rdi];
|
||||
if (outAddress == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if (ResolveCurrentFiberAddress(ctx) == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
return TryWriteUInt64(ctx, outAddress, ctx[CpuRegister.Rbp])
|
||||
? SetReturn(ctx, 0)
|
||||
: SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.TryWriteUInt64(outAddress, ctx[CpuRegister.Rbp])
|
||||
? ctx.SetReturn(0)
|
||||
: ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
private static int FiberInitializeCore(
|
||||
@@ -459,37 +459,37 @@ public static class FiberExports
|
||||
{
|
||||
if (fiber == 0 || nameAddress == 0 || entry == 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorNull);
|
||||
return ctx.SetReturn(FiberErrorNull);
|
||||
}
|
||||
|
||||
if ((fiber & 7) != 0 ||
|
||||
(contextAddress & 15) != 0 ||
|
||||
(optParam & 7) != 0)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorAlignment);
|
||||
return ctx.SetReturn(FiberErrorAlignment);
|
||||
}
|
||||
|
||||
if (contextSize != 0 && contextSize < FiberContextMinimumSize)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorRange);
|
||||
return ctx.SetReturn(FiberErrorRange);
|
||||
}
|
||||
|
||||
if ((contextSize & 15) != 0 ||
|
||||
(contextAddress == 0 && contextSize != 0) ||
|
||||
(contextAddress != 0 && contextSize == 0))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -497,27 +497,27 @@ public static class FiberExports
|
||||
flags |= FiberFlagContextSizeCheck;
|
||||
}
|
||||
|
||||
if (!TryWriteUInt32(ctx, fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateIdle) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberEntryOffset, entry) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||
if (!ctx.TryWriteUInt32(fiber + FiberMagicStartOffset, FiberSignature0) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateIdle) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberEntryOffset, entry) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberArgOnInitializeOffset, argOnInitialize) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!TryWriteName(ctx, fiber + FiberNameOffset, name) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextPointerOffset, 0) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberFlagsOffset, flags) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||
!TryWriteUInt32(ctx, fiber + FiberMagicEndOffset, FiberSignature1))
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextPointerOffset, 0) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberFlagsOffset, flags) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress == 0 ? 0 : contextAddress + contextSize) ||
|
||||
!ctx.TryWriteUInt32(fiber + FiberMagicEndOffset, FiberSignature1))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
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)
|
||||
@@ -532,7 +532,7 @@ public static class FiberExports
|
||||
}
|
||||
|
||||
TraceFiber($"init fiber=0x{fiber:X16} entry=0x{entry:X16} ctx=0x{contextAddress:X16} size=0x{contextSize:X} name='{name}'");
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static int FiberRunCore(
|
||||
@@ -547,12 +547,12 @@ public static class FiberExports
|
||||
{
|
||||
if (!TryValidateFiber(ctx, fiber, out var error))
|
||||
{
|
||||
return SetReturn(ctx, error);
|
||||
return ctx.SetReturn(error);
|
||||
}
|
||||
|
||||
if (!TryReadFiberFields(ctx, fiber, out var fields))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (attachContextAddress != 0 || attachContextSize != 0)
|
||||
@@ -560,7 +560,7 @@ public static class FiberExports
|
||||
var attachResult = AttachContext(ctx, fiber, attachContextAddress, attachContextSize, ref fields);
|
||||
if (attachResult != 0)
|
||||
{
|
||||
return SetReturn(ctx, attachResult);
|
||||
return ctx.SetReturn(attachResult);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -568,16 +568,16 @@ public static class FiberExports
|
||||
if ((isSwitch && previousFiber == 0) ||
|
||||
(!isSwitch && previousFiber != 0))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
if (previousFiber == fiber)
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
if (GuestThreadExecution.Scheduler is not { SupportsGuestContextTransfer: true } ||
|
||||
!GuestThreadExecution.TryGetCurrentImportCallFrame(out var frame))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorPermission);
|
||||
return ctx.SetReturn(FiberErrorPermission);
|
||||
}
|
||||
|
||||
GuestCpuContinuation transferTarget;
|
||||
@@ -586,12 +586,12 @@ public static class FiberExports
|
||||
{
|
||||
if (!TryReadFiberFields(ctx, fiber, out fields))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
if (fields.State != FiberStateIdle)
|
||||
{
|
||||
TraceFiber($"run-state-error reason={reason} fiber=0x{fiber:X16} state=0x{fields.State:X8}");
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
FiberContinuation targetContinuation;
|
||||
@@ -602,12 +602,12 @@ public static class FiberExports
|
||||
}
|
||||
else if (!TryCreateInitialContinuation(ctx, fields, argOnRun, out targetContinuation))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed && !TryWriteResumeArgument(ctx, targetContinuation, argOnRun))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
var callerContinuation = new FiberContinuation(
|
||||
@@ -616,11 +616,11 @@ public static class FiberExports
|
||||
|
||||
if (previousFiber != 0)
|
||||
{
|
||||
if (!TryReadUInt32(ctx, previousFiber + FiberStateOffset, out var previousState) ||
|
||||
if (!ctx.TryReadUInt32(previousFiber + FiberStateOffset, out var previousState) ||
|
||||
previousState != FiberStateRun ||
|
||||
!TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateIdle))
|
||||
!ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateIdle))
|
||||
{
|
||||
return SetReturn(ctx, FiberErrorState);
|
||||
return ctx.SetReturn(FiberErrorState);
|
||||
}
|
||||
|
||||
_continuations[previousFiber] = callerContinuation;
|
||||
@@ -631,15 +631,15 @@ public static class FiberExports
|
||||
_returnTargets[fiber] = new FiberReturnTarget(0, callerContinuation);
|
||||
}
|
||||
|
||||
if (!TryWriteUInt32(ctx, fiber + FiberStateOffset, FiberStateRun))
|
||||
if (!ctx.TryWriteUInt32(fiber + FiberStateOffset, FiberStateRun))
|
||||
{
|
||||
if (previousFiber != 0)
|
||||
{
|
||||
_continuations.TryRemove(previousFiber, out _);
|
||||
_ = TryWriteUInt32(ctx, previousFiber + FiberStateOffset, FiberStateRun);
|
||||
_ = ctx.TryWriteUInt32(previousFiber + FiberStateOffset, FiberStateRun);
|
||||
}
|
||||
_returnTargets.TryRemove(fiber, out _);
|
||||
return SetReturn(ctx, FiberErrorInvalid);
|
||||
return ctx.SetReturn(FiberErrorInvalid);
|
||||
}
|
||||
|
||||
if (resumed)
|
||||
@@ -656,7 +656,7 @@ public static class FiberExports
|
||||
TraceFiber(
|
||||
$"transfer reason={reason} from=0x{previousFiber:X16} to=0x{fiber:X16} resume={resumed} " +
|
||||
$"rip=0x{transferTarget.Rip:X16} rsp=0x{transferTarget.Rsp:X16} arg=0x{argOnRun:X16}");
|
||||
return SetReturn(ctx, 0);
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryCreateInitialContinuation(
|
||||
@@ -673,7 +673,7 @@ public static class FiberExports
|
||||
|
||||
var stackEnd = fields.ContextAddress + fields.ContextSize;
|
||||
var entryRsp = (stackEnd & ~15UL) - sizeof(ulong);
|
||||
if (!TryWriteUInt64(ctx, entryRsp, 0))
|
||||
if (!ctx.TryWriteUInt64(entryRsp, 0))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
@@ -708,7 +708,7 @@ public static class FiberExports
|
||||
FiberContinuation continuation,
|
||||
ulong argument) =>
|
||||
continuation.ArgOnRunAddress == 0 ||
|
||||
TryWriteUInt64(ctx, continuation.ArgOnRunAddress, argument);
|
||||
ctx.TryWriteUInt64(continuation.ArgOnRunAddress, argument);
|
||||
|
||||
private static GuestCpuContinuation CaptureContinuation(
|
||||
CpuContext ctx,
|
||||
@@ -779,11 +779,11 @@ public static class FiberExports
|
||||
return FiberErrorInvalid;
|
||||
}
|
||||
|
||||
if (!TryWriteUInt64(ctx, fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!TryWriteUInt64(ctx, fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||
!TryWriteUInt64(ctx, contextAddress, FiberStackSignature))
|
||||
if (!ctx.TryWriteUInt64(fiber + FiberContextAddressOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextSizeOffset, contextSize) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextStartOffset, contextAddress) ||
|
||||
!ctx.TryWriteUInt64(fiber + FiberContextEndOffset, contextAddress + contextSize) ||
|
||||
!ctx.TryWriteUInt64(contextAddress, FiberStackSignature))
|
||||
{
|
||||
return FiberErrorInvalid;
|
||||
}
|
||||
@@ -839,8 +839,8 @@ public static class FiberExports
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!TryReadUInt32(ctx, fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||
!TryReadUInt32(ctx, fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||
if (!ctx.TryReadUInt32(fiber + FiberMagicStartOffset, out var magicStart) ||
|
||||
!ctx.TryReadUInt32(fiber + FiberMagicEndOffset, out var magicEnd) ||
|
||||
magicStart != FiberSignature0 ||
|
||||
magicEnd != FiberSignature1)
|
||||
{
|
||||
@@ -855,12 +855,12 @@ public static class FiberExports
|
||||
private static bool TryReadFiberFields(CpuContext ctx, ulong fiber, out FiberFields fields)
|
||||
{
|
||||
fields = default;
|
||||
if (!TryReadUInt32(ctx, fiber + FiberStateOffset, out var state) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberEntryOffset, out var entry) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||
!TryReadUInt64(ctx, fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||
!TryReadUInt32(ctx, fiber + FiberFlagsOffset, out var flags) ||
|
||||
if (!ctx.TryReadUInt32(fiber + FiberStateOffset, out var state) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberEntryOffset, out var entry) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberArgOnInitializeOffset, out var argOnInitialize) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberContextAddressOffset, out var contextAddress) ||
|
||||
!ctx.TryReadUInt64(fiber + FiberContextSizeOffset, out var contextSize) ||
|
||||
!ctx.TryReadUInt32(fiber + FiberFlagsOffset, out var flags) ||
|
||||
!TryReadInlineName(ctx, fiber + FiberNameOffset, out var name))
|
||||
{
|
||||
return false;
|
||||
@@ -898,46 +898,6 @@ public static class FiberExports
|
||||
return 0;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt64(CpuContext ctx, ulong address, ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
BinaryPrimitives.WriteUInt64LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryWriteName(CpuContext ctx, ulong address, string name)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[MaxNameLength + 1];
|
||||
@@ -965,37 +925,6 @@ public static class FiberExports
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||
{
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||
{
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes[index] = current[0];
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, int result)
|
||||
{
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void TraceFiber(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_FIBER"), "1", StringComparison.Ordinal))
|
||||
|
||||
@@ -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.Libs.Ampr;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Concurrent;
|
||||
using System.Threading;
|
||||
|
||||
@@ -50,7 +49,7 @@ public static class KernelAprCompatExports
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
if (outSubmissionId != 0 && !TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||
if (outSubmissionId != 0 && !ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -141,7 +140,7 @@ public static class KernelAprCompatExports
|
||||
return completionResult;
|
||||
}
|
||||
|
||||
if (!TryWriteUInt32(ctx, outSubmissionId, submissionId))
|
||||
if (!ctx.TryWriteUInt32(outSubmissionId, submissionId))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -150,6 +149,19 @@ public static class KernelAprCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
// Success stub: the argument layout is unknown and callers tolerate the
|
||||
// empty answer (Quake streams fine), so no output payload is written until
|
||||
// the real signature is reversed.
|
||||
[SysAbiExport(
|
||||
Nid = "WvEu7yl3Ivg",
|
||||
ExportName = "sceKernelAprGetFileSize",
|
||||
Target = Generation.Gen4 | Generation.Gen5,
|
||||
LibraryName = "libKernel")]
|
||||
public static int KernelAprGetFileSize(CpuContext ctx)
|
||||
{
|
||||
return ctx.SetReturn(0);
|
||||
}
|
||||
|
||||
private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress)
|
||||
{
|
||||
Span<byte> result = stackalloc byte[sizeof(ulong)];
|
||||
@@ -178,13 +190,6 @@ public static class KernelAprCompatExports
|
||||
return tag is 0x0C or 0x10;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static void TraceApr(
|
||||
CpuContext ctx,
|
||||
string operation,
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
// 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;
|
||||
@@ -52,17 +51,17 @@ public static class KernelEventFlagCompatExports
|
||||
optionAddress != 0 ||
|
||||
!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)
|
||||
{
|
||||
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));
|
||||
@@ -76,11 +75,11 @@ public static class KernelEventFlagCompatExports
|
||||
if (!ctx.TryWriteUInt64(outAddress, handle))
|
||||
{
|
||||
_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}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -93,7 +92,7 @@ public static class KernelEventFlagCompatExports
|
||||
var handle = ctx[CpuRegister.Rdi];
|
||||
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)
|
||||
@@ -102,7 +101,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
TraceEventFlag($"delete handle=0x{handle:X16} name='{state.Name}'");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -117,7 +116,7 @@ public static class KernelEventFlagCompatExports
|
||||
var returnRip = GetCurrentReturnRip();
|
||||
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)
|
||||
@@ -128,7 +127,7 @@ public static class KernelEventFlagCompatExports
|
||||
}
|
||||
|
||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetEventFlagWakeKey(handle));
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
|
||||
[SysAbiExport(
|
||||
@@ -142,7 +141,7 @@ public static class KernelEventFlagCompatExports
|
||||
var pattern = ctx[CpuRegister.Rsi];
|
||||
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)
|
||||
@@ -151,7 +150,7 @@ public static class KernelEventFlagCompatExports
|
||||
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(
|
||||
@@ -168,29 +167,29 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
lock (state.Gate)
|
||||
{
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||
}
|
||||
|
||||
ApplyClearMode(state, pattern, waitMode);
|
||||
TraceEventFlag($"poll handle=0x{handle:X16} pattern=0x{pattern:X16} mode=0x{waitMode:X2} bits=0x{state.Bits:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -210,18 +209,18 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
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))
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||
}
|
||||
|
||||
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);
|
||||
@@ -229,15 +228,15 @@ public static class KernelEventFlagCompatExports
|
||||
{
|
||||
if (TryCompleteSatisfiedWait(ctx, state, pattern, waitMode, resultAddress, out var immediateWaitResult))
|
||||
{
|
||||
return SetReturn(ctx, immediateWaitResult);
|
||||
return ctx.SetReturn(immediateWaitResult);
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0)
|
||||
{
|
||||
_ = TryWriteUInt32(ctx, timeoutAddress, 0);
|
||||
_ = ctx.TryWriteUInt32(timeoutAddress, 0);
|
||||
_ = TryWriteResultPattern(ctx, resultAddress, state.Bits);
|
||||
TraceEventFlag($"wait-timeout handle=0x{handle:X16} pattern=0x{pattern:X16} timeout={timeoutUsec} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||
}
|
||||
|
||||
var currentGuestThread = GuestThreadExecution.CurrentGuestThreadHandle;
|
||||
@@ -272,7 +271,7 @@ public static class KernelEventFlagCompatExports
|
||||
var scheduler = GuestThreadExecution.Scheduler;
|
||||
if (scheduler is null)
|
||||
{
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||
}
|
||||
|
||||
state.WaitingThreads++;
|
||||
@@ -297,7 +296,7 @@ public static class KernelEventFlagCompatExports
|
||||
state.WaitingThreads = Math.Max(0, state.WaitingThreads - 1);
|
||||
releaseWaiter = false;
|
||||
TraceEventFlag($"wait-wake handle=0x{handle:X16} pattern=0x{pattern:X16} bits=0x{state.Bits:X16} waiters={state.WaitingThreads} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, pumpedWaitResult);
|
||||
return ctx.SetReturn(pumpedWaitResult);
|
||||
}
|
||||
|
||||
Monitor.Wait(state.Gate, HostWaitPumpMilliseconds);
|
||||
@@ -314,7 +313,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
state.WaitingThreads++;
|
||||
TraceEventFlag($"wait-block handle=0x{handle:X16} pattern=0x{pattern:X16} waiters={state.WaitingThreads} guest_thread=0x{currentGuestThread:X16} fiber=0x{currentFiber:X16} managed={managedThread} ret=0x{returnRip:X16}");
|
||||
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -334,15 +333,15 @@ public static class KernelEventFlagCompatExports
|
||||
var waiterCountAddress = ctx[CpuRegister.Rdx];
|
||||
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)
|
||||
{
|
||||
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;
|
||||
@@ -351,7 +350,7 @@ public static class KernelEventFlagCompatExports
|
||||
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)
|
||||
@@ -453,84 +452,6 @@ public static class KernelEventFlagCompatExports
|
||||
private static bool TryWriteResultPattern(CpuContext ctx, ulong address, ulong bits) =>
|
||||
address == 0 || ctx.TryWriteUInt64(address, bits);
|
||||
|
||||
private static bool TryReadUInt32(CpuContext ctx, ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadUInt64(CpuContext ctx, ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt64LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryReadByte(CpuContext ctx, ulong address, out byte value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = buffer[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int capacity, out string value)
|
||||
{
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
Span<byte> current = stackalloc byte[1];
|
||||
if (!ctx.Memory.TryRead(address + (ulong)index, current))
|
||||
{
|
||||
value = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
if (current[0] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
}
|
||||
|
||||
bytes[index] = current[0];
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||
{
|
||||
var value = (int)result;
|
||||
ctx[CpuRegister.Rax] = unchecked((ulong)value);
|
||||
return value;
|
||||
}
|
||||
|
||||
private static void TraceEventFlag(string message)
|
||||
{
|
||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_EVENT_FLAG"), "1", StringComparison.Ordinal))
|
||||
@@ -614,7 +535,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
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}");
|
||||
}
|
||||
@@ -622,7 +543,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
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}");
|
||||
}
|
||||
@@ -630,7 +551,7 @@ public static class KernelEventFlagCompatExports
|
||||
|
||||
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}");
|
||||
}
|
||||
|
||||
@@ -276,8 +276,24 @@ public static class KernelEventQueueCompatExports
|
||||
var outCountAddress = ctx[CpuRegister.Rcx];
|
||||
var timeoutAddress = ctx[CpuRegister.R8];
|
||||
|
||||
if (!IsValidEqueue(handle))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
}
|
||||
|
||||
if (eventsAddress == 0 || eventCapacity < 1)
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||
}
|
||||
|
||||
uint timeoutUsec = 0;
|
||||
if (timeoutAddress != 0 && !ctx.TryReadUInt32(timeoutAddress, out timeoutUsec))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
var deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !TryWriteUInt32(ctx, outCountAddress, (uint)deliveredCount))
|
||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
@@ -300,6 +316,41 @@ public static class KernelEventQueueCompatExports
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
if (timeoutAddress != 0 && ctx.TryReadUInt64(timeoutAddress, out var timeoutRaw))
|
||||
{
|
||||
var timeoutMicros = timeoutRaw & 0xFFFF_FFFFUL;
|
||||
var deadline = Environment.TickCount64 +
|
||||
Math.Max(1L, (long)Math.Min(timeoutMicros / 1000, int.MaxValue));
|
||||
lock (_eventQueueGate)
|
||||
{
|
||||
while (!HasPendingEvents(handle))
|
||||
{
|
||||
var remaining = deadline - Environment.TickCount64;
|
||||
if (remaining <= 0)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
Monitor.Wait(_eventQueueGate, (int)Math.Min(remaining, 100));
|
||||
}
|
||||
}
|
||||
|
||||
deliveredCount = DequeueEvents(ctx, handle, eventsAddress, eventCapacity);
|
||||
if (outCountAddress != 0 && !ctx.TryWriteUInt32(outCountAddress, (uint)deliveredCount))
|
||||
{
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||
}
|
||||
|
||||
if (deliveredCount > 0)
|
||||
{
|
||||
TraceEventQueue(ctx, "wait-timed-deliver", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
|
||||
TraceEventQueue(ctx, "wait-timeout", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
}
|
||||
|
||||
TraceEventQueue(ctx, "wait", handle);
|
||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||
}
|
||||
@@ -330,6 +381,7 @@ public static class KernelEventQueueCompatExports
|
||||
|
||||
queue.AddLast(queuedEvent);
|
||||
queued = true;
|
||||
Monitor.PulseAll(_eventQueueGate);
|
||||
}
|
||||
|
||||
if (queued)
|
||||
@@ -489,12 +541,14 @@ public static class KernelEventQueueCompatExports
|
||||
ulong outCountAddress)
|
||||
{
|
||||
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_OK;
|
||||
return deliveredCount > 0
|
||||
? (int)OrbisGen2Result.ORBIS_GEN2_OK
|
||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||
}
|
||||
|
||||
private static bool HasPendingEvents(ulong handle)
|
||||
@@ -600,16 +654,8 @@ public static class KernelEventQueueCompatExports
|
||||
return;
|
||||
}
|
||||
|
||||
var returnRip = 0UL;
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out returnRip);
|
||||
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out ulong returnRip);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] equeue.{operation}: handle=0x{handle:X16} rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} ret=0x{returnRip:X16}");
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext ctx, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return ctx.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user