mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 50f78f3713 | |||
| 63b440efcd | |||
| 0565d01744 | |||
| 56bf00e9a5 | |||
| 46b729c5b4 | |||
| 4b7df8623a | |||
| 3c2134474d | |||
| 298ef01809 | |||
| 2060dacaf1 | |||
| f73c9e8c3f | |||
| 3d2c30b151 | |||
| 61d28e9e08 | |||
| 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 |
@@ -16,6 +16,11 @@ body:
|
|||||||
Example title:
|
Example title:
|
||||||
Demon’s Souls (PPSA01341)
|
Demon’s Souls (PPSA01341)
|
||||||
|
|
||||||
|
**Before reporting:**
|
||||||
|
- Test the game on a **fresh build from `main`**, not an old release.
|
||||||
|
- Copy the **exact commit SHA** shown at the top of the log.
|
||||||
|
- `DEBUG` and `TRACE` lines are diagnostic context, not errors by themselves; report the last milestone or the first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop.
|
||||||
|
|
||||||
- type: input
|
- type: input
|
||||||
id: game_name
|
id: game_name
|
||||||
attributes:
|
attributes:
|
||||||
@@ -73,11 +78,20 @@ body:
|
|||||||
id: logs
|
id: logs
|
||||||
attributes:
|
attributes:
|
||||||
label: Log File
|
label: Log File
|
||||||
description: Drag and drop your log file here, or click to browse
|
description: Attach the **full** log file (`.log`, `.txt`, or `.zip`). Do not paste only a fragment — the maintainer needs the complete log from launch to crash.
|
||||||
validations:
|
validations:
|
||||||
required: false
|
required: true
|
||||||
accept: ".log,.txt,.zip"
|
accept: ".log,.txt,.zip"
|
||||||
|
|
||||||
|
- type: input
|
||||||
|
id: last_milestone_first_error
|
||||||
|
attributes:
|
||||||
|
label: Last Milestone / First Error
|
||||||
|
description: Paste the **last useful milestone line** or the **first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop** from the log. `DEBUG`/`TRACE` lines do not count unless a real error follows them.
|
||||||
|
placeholder: "e.g. CpuEngine: native-only OR [ERROR] Native backend FAILED: ..."
|
||||||
|
validations:
|
||||||
|
required: true
|
||||||
|
|
||||||
- type: dropdown
|
- type: dropdown
|
||||||
id: operating_system
|
id: operating_system
|
||||||
attributes:
|
attributes:
|
||||||
@@ -119,7 +133,8 @@ body:
|
|||||||
id: emulator_version
|
id: emulator_version
|
||||||
attributes:
|
attributes:
|
||||||
label: Emulator Version / Commit
|
label: Emulator Version / Commit
|
||||||
placeholder: e.g. v0.0.1 / a1b2c3d
|
description: Copy the **exact short SHA** from the top of the log. Do not enter just `0.0.1` without a commit hash.
|
||||||
|
placeholder: e.g. a1b2c3d
|
||||||
validations:
|
validations:
|
||||||
required: true
|
required: true
|
||||||
|
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 120 KiB After Width: | Height: | Size: 101 KiB |
@@ -39,3 +39,4 @@ Desktop.ini
|
|||||||
ehthumbs.db
|
ehthumbs.db
|
||||||
|
|
||||||
.vs/
|
.vs/
|
||||||
|
.idea/
|
||||||
|
|||||||
@@ -0,0 +1,70 @@
|
|||||||
|
<!--
|
||||||
|
Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
-->
|
||||||
|
|
||||||
|
# Contributing
|
||||||
|
|
||||||
|
Contributions are always welcome!
|
||||||
|
|
||||||
|
Before opening a pull request, please keep the following in mind:
|
||||||
|
|
||||||
|
- Keep PRs small and focused on a single topic.
|
||||||
|
- Discuss large architectural changes before implementing them.
|
||||||
|
- Follow the project's existing coding style.
|
||||||
|
- Do not submit generated code unless you fully understand and can maintain it.
|
||||||
|
- Do not introduce Sony proprietary code, firmware, keys, decrypted assets, or other copyrighted PlayStation materials.
|
||||||
|
- All reverse engineering should be based on publicly available information, clean-room techniques, or your own original research.
|
||||||
|
- Game-specific hacks should be avoided whenever possible. Prefer generic implementations that improve overall compatibility.
|
||||||
|
- New features should not break existing behavior.
|
||||||
|
- Ensure the project builds successfully before submitting a PR.
|
||||||
|
|
||||||
|
If you're unsure about a design decision, feel free to open a discussion or draft PR first.
|
||||||
|
|
||||||
|
## AI-Assisted Contributions
|
||||||
|
|
||||||
|
AI-assisted development is welcome and may be used for research, reverse engineering, code generation, or documentation.
|
||||||
|
|
||||||
|
However, contributors are expected to fully understand every line of code they submit. By opening a pull request, you confirm that you are able to explain, modify, debug, and maintain the submitted code without relying on the AI that generated it.
|
||||||
|
|
||||||
|
When submitting an AI-assisted PR:
|
||||||
|
|
||||||
|
- Clearly explain **what the change does**, **why it is needed**, and **what problem it solves**, using your own words.
|
||||||
|
- Describe **how you verified the change**, including the games, applications, or test cases used.
|
||||||
|
- Avoid excessive product-level logging. Use logging only when it provides meaningful diagnostic value.
|
||||||
|
- Comments should document design decisions or implementation details in your own words. Avoid generic AI-generated comments that merely restate what the code already does.
|
||||||
|
- Be prepared to answer review questions about the implementation. "The AI generated it" is not considered a sufficient explanation.
|
||||||
|
- Large AI-generated changes without a clear understanding of the implementation are unlikely to be accepted.
|
||||||
|
- If the implementation cannot be reasonably explained during code review, the pull request may be rejected regardless of whether it works.
|
||||||
|
|
||||||
|
The quality, correctness, maintainability, and long-term ownership of the submitted code remain the responsibility of the contributor.
|
||||||
|
|
||||||
|
## Coding Style
|
||||||
|
|
||||||
|
SharpEmu follows a consistent coding style across the project. Please ensure your contributions match the existing style.
|
||||||
|
|
||||||
|
- Use **4 spaces** for indentation (no tabs).
|
||||||
|
- Use **2 spaces** for XML-based files (e.g. `.csproj`, `.props`, `.targets`, `.xml`, `yml`, GitHub workflow files where applicable).
|
||||||
|
- Respect the project's `.editorconfig`.
|
||||||
|
- Ensure every text file ends with a **single trailing newline**
|
||||||
|
- Avoid formatting-only commits unless they are the purpose of the PR.
|
||||||
|
- Keep naming, formatting, and file organization consistent with the surrounding code.
|
||||||
|
- Prefer small, focused changes over large refactors.
|
||||||
|
|
||||||
|
### REUSE Compliance
|
||||||
|
|
||||||
|
This repository follows the REUSE Specification.
|
||||||
|
|
||||||
|
Every new file must contain the appropriate SPDX license header. Pull requests that do not comply with the project's REUSE requirements will fail CI and will not be merged.
|
||||||
|
|
||||||
|
### Recommended Development Environment
|
||||||
|
|
||||||
|
The repository includes an `.editorconfig` and Visual Studio solution files.
|
||||||
|
|
||||||
|
For the best experience, we recommend using:
|
||||||
|
|
||||||
|
- Visual Studio Code with;
|
||||||
|
- C#
|
||||||
|
- C# Dev Kit
|
||||||
|
|
||||||
|
Most editors that support `.editorconfig` will automatically apply the project's formatting rules.
|
||||||
@@ -32,4 +32,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<DebugType>none</DebugType>
|
<DebugType>none</DebugType>
|
||||||
<DebugSymbols>false</DebugSymbols>
|
<DebugSymbols>false</DebugSymbols>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Build provenance surfaced by SharpEmu.Logging.BuildInfo as a banner at the
|
||||||
|
top of the log. Populated from GitHub Actions environment variables when
|
||||||
|
building in CI; empty locally. -->
|
||||||
|
<ItemGroup>
|
||||||
|
<AssemblyMetadata Include="SharpEmu.BuildConfiguration" Value="$(Configuration)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_SHA)' != ''" Include="SharpEmu.BuildSha" Value="$(GITHUB_SHA)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_REF_NAME)' != ''" Include="SharpEmu.BuildBranch" Value="$(GITHUB_REF_NAME)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_REF)' != ''" Include="SharpEmu.BuildRef" Value="$(GITHUB_REF)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_EVENT_NAME)' != ''" Include="SharpEmu.BuildEventName" Value="$(GITHUB_EVENT_NAME)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_REPOSITORY)' != ''" Include="SharpEmu.BuildRepository" Value="$(GITHUB_REPOSITORY)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_SERVER_URL)' != ''" Include="SharpEmu.BuildServerUrl" Value="$(GITHUB_SERVER_URL)" />
|
||||||
|
<AssemblyMetadata Condition="'$(GITHUB_RUN_ID)' != ''" Include="SharpEmu.BuildRunId" Value="$(GITHUB_RUN_ID)" />
|
||||||
|
</ItemGroup>
|
||||||
</Project>
|
</Project>
|
||||||
|
|||||||
@@ -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.
|
||||||
@@ -15,3 +15,9 @@ path = [
|
|||||||
precedence = "aggregate"
|
precedence = "aggregate"
|
||||||
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||||
SPDX-License-Identifier = "GPL-2.0-or-later"
|
SPDX-License-Identifier = "GPL-2.0-or-later"
|
||||||
|
|
||||||
|
[[annotations]]
|
||||||
|
path = "src/SharpEmu.GUI/Atrac9/**"
|
||||||
|
precedence = "aggregate"
|
||||||
|
SPDX-FileCopyrightText = "2018 Alex Barney"
|
||||||
|
SPDX-License-Identifier = "MIT"
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"sdk": {
|
"sdk": {
|
||||||
"version": "10.0.103",
|
"version": "10.0.103",
|
||||||
"rollForward": "latestFeature"
|
"rollForward": "disable"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+417
-13
@@ -8,12 +8,15 @@ using SharpEmu.HLE;
|
|||||||
using SharpEmu.Logging;
|
using SharpEmu.Logging;
|
||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace SharpEmu.CLI;
|
namespace SharpEmu.CLI;
|
||||||
|
|
||||||
internal static partial class Program
|
internal static partial class Program
|
||||||
{
|
{
|
||||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.CLI");
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.CLI");
|
||||||
|
private static readonly object ConsoleMirrorSync = new();
|
||||||
|
private static StreamWriter? _consoleMirrorFile;
|
||||||
private const int DefaultImportTraceLimit = 32;
|
private const int DefaultImportTraceLimit = 32;
|
||||||
private const string MitigatedChildFlag = "--sharpemu-mitigated-child";
|
private const string MitigatedChildFlag = "--sharpemu-mitigated-child";
|
||||||
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
|
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
|
||||||
@@ -21,11 +24,15 @@ internal static partial class Program
|
|||||||
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
|
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
|
||||||
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
||||||
private const int JobObjectExtendedLimitInformation = 9;
|
private const int JobObjectExtendedLimitInformation = 9;
|
||||||
|
private const int STARTF_USESTDHANDLES = 0x00000100;
|
||||||
|
private const uint HANDLE_FLAG_INHERIT = 0x00000001;
|
||||||
|
private const string MitigatedChildEnvironment = "SHARPEMU_MITIGATED_CHILD";
|
||||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
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_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
|
||||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
||||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||||
private const int ATTACH_PARENT_PROCESS = -1;
|
private const int ATTACH_PARENT_PROCESS = -1;
|
||||||
|
private const int STD_INPUT_HANDLE = -10;
|
||||||
private const int STD_OUTPUT_HANDLE = -11;
|
private const int STD_OUTPUT_HANDLE = -11;
|
||||||
private const int STD_ERROR_HANDLE = -12;
|
private const int STD_ERROR_HANDLE = -12;
|
||||||
private const uint GENERIC_READ = 0x80000000;
|
private const uint GENERIC_READ = 0x80000000;
|
||||||
@@ -43,6 +50,7 @@ internal static partial class Program
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
DropConsoleFileMirror();
|
||||||
SharpEmuLog.Shutdown();
|
SharpEmuLog.Shutdown();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -60,6 +68,11 @@ internal static partial class Program
|
|||||||
// The executable uses the GUI subsystem, so CLI mode has to connect
|
// The executable uses the GUI subsystem, so CLI mode has to connect
|
||||||
// itself to a console before the first write.
|
// itself to a console before the first write.
|
||||||
EnsureCliConsole();
|
EnsureCliConsole();
|
||||||
|
UseUtf8ConsoleOutput();
|
||||||
|
if (isMitigatedChild && TryGetLogFileArgument(args, out var earlyLogFilePath))
|
||||||
|
{
|
||||||
|
TryEnableConsoleFileMirror(earlyLogFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||||
|
|
||||||
@@ -68,14 +81,21 @@ internal static partial class Program
|
|||||||
return childExitCode;
|
return childExitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel))
|
if (!TryParseArguments(args, out var ebootPath, out var runtimeOptions, out var logLevel, out var logFilePath))
|
||||||
{
|
{
|
||||||
PrintUsage();
|
PrintUsage();
|
||||||
return 1;
|
return 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!isMitigatedChild && !string.IsNullOrWhiteSpace(logFilePath))
|
||||||
|
{
|
||||||
|
TryEnableConsoleFileMirror(logFilePath);
|
||||||
|
}
|
||||||
|
|
||||||
SharpEmuLog.MinimumLevel = logLevel;
|
SharpEmuLog.MinimumLevel = logLevel;
|
||||||
|
|
||||||
|
Log.Info(BuildInfo.Banner);
|
||||||
|
|
||||||
ebootPath = Path.GetFullPath(ebootPath);
|
ebootPath = Path.GetFullPath(ebootPath);
|
||||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||||
|
|
||||||
@@ -160,6 +180,48 @@ internal static partial class Program
|
|||||||
RebindStdHandleToConsole(STD_ERROR_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)
|
private static void RebindStdHandleToConsole(int stdHandle)
|
||||||
{
|
{
|
||||||
if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0)
|
if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0)
|
||||||
@@ -189,6 +251,10 @@ internal static partial class Program
|
|||||||
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
||||||
{
|
{
|
||||||
isMitigatedChild = false;
|
isMitigatedChild = false;
|
||||||
|
var trustedMitigatedChild = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable(MitigatedChildEnvironment),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
return args;
|
return args;
|
||||||
@@ -199,7 +265,7 @@ internal static partial class Program
|
|||||||
{
|
{
|
||||||
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
|
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
isMitigatedChild = true;
|
isMitigatedChild = trustedMitigatedChild;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -238,9 +304,11 @@ internal static partial class Program
|
|||||||
var commandLine = BuildCommandLine(processPath, childArgs);
|
var commandLine = BuildCommandLine(processPath, childArgs);
|
||||||
var startupInfoEx = new STARTUPINFOEX();
|
var startupInfoEx = new STARTUPINFOEX();
|
||||||
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
|
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
|
||||||
|
ConfigureInheritedStdHandles(ref startupInfoEx.StartupInfo);
|
||||||
|
|
||||||
nint attributeList = 0;
|
nint attributeList = 0;
|
||||||
nint mitigationPolicies = 0;
|
nint mitigationPolicies = 0;
|
||||||
|
var previousChildEnvironment = Environment.GetEnvironmentVariable(MitigatedChildEnvironment);
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
nuint attributeListSize = 0;
|
nuint attributeListSize = 0;
|
||||||
@@ -248,7 +316,9 @@ internal static partial class Program
|
|||||||
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
|
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
|
||||||
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
|
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
|
||||||
{
|
{
|
||||||
return false;
|
childExitCode = 5;
|
||||||
|
Console.Error.WriteLine($"[ERROR] Failed to initialize mitigation attributes: {Marshal.GetLastWin32Error()}");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
startupInfoEx.lpAttributeList = attributeList;
|
startupInfoEx.lpAttributeList = attributeList;
|
||||||
@@ -256,8 +326,7 @@ internal static partial class Program
|
|||||||
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||||
var policy2 =
|
var policy2 =
|
||||||
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
|
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_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
|
||||||
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
|
||||||
|
|
||||||
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
||||||
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
||||||
@@ -272,24 +341,31 @@ internal static partial class Program
|
|||||||
0,
|
0,
|
||||||
0))
|
0))
|
||||||
{
|
{
|
||||||
return false;
|
childExitCode = 5;
|
||||||
|
Console.Error.WriteLine($"[ERROR] Failed to apply mitigation attributes: {Marshal.GetLastWin32Error()}");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
var cmdLineBuilder = new StringBuilder(commandLine);
|
var cmdLineBuilder = new StringBuilder(commandLine);
|
||||||
nint jobHandle = 0;
|
nint jobHandle = 0;
|
||||||
if (!CreateProcessW(
|
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||||
|
var created = CreateProcessW(
|
||||||
processPath,
|
processPath,
|
||||||
cmdLineBuilder,
|
cmdLineBuilder,
|
||||||
0,
|
0,
|
||||||
0,
|
0,
|
||||||
false,
|
true,
|
||||||
EXTENDED_STARTUPINFO_PRESENT,
|
EXTENDED_STARTUPINFO_PRESENT,
|
||||||
0,
|
0,
|
||||||
Environment.CurrentDirectory,
|
Environment.CurrentDirectory,
|
||||||
ref startupInfoEx,
|
ref startupInfoEx,
|
||||||
out var processInfo))
|
out var processInfo);
|
||||||
|
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
|
||||||
|
if (!created)
|
||||||
{
|
{
|
||||||
return false;
|
childExitCode = 5;
|
||||||
|
Console.Error.WriteLine($"[ERROR] Failed to launch mitigated child process: {Marshal.GetLastWin32Error()}");
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -343,6 +419,8 @@ internal static partial class Program
|
|||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
|
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
|
||||||
|
|
||||||
if (attributeList != 0)
|
if (attributeList != 0)
|
||||||
{
|
{
|
||||||
DeleteProcThreadAttributeList(attributeList);
|
DeleteProcThreadAttributeList(attributeList);
|
||||||
@@ -356,6 +434,238 @@ internal static partial class Program
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryGetLogFileArgument(IReadOnlyList<string> args, out string path)
|
||||||
|
{
|
||||||
|
for (var i = 0; i < args.Count; i++)
|
||||||
|
{
|
||||||
|
var argument = args[i];
|
||||||
|
if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (i + 1 < args.Count &&
|
||||||
|
!string.IsNullOrWhiteSpace(args[i + 1]) &&
|
||||||
|
!args[i + 1].StartsWith("--", StringComparison.Ordinal) &&
|
||||||
|
ShouldConsumeLogFilePath(args, i + 1))
|
||||||
|
{
|
||||||
|
path = args[i + 1];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
path = BuildDefaultLogFilePath(TryFindEbootPathToken(args));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const string logFilePrefix = "--log-file=";
|
||||||
|
if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
!string.IsNullOrWhiteSpace(argument[logFilePrefix.Length..]))
|
||||||
|
{
|
||||||
|
path = argument[logFilePrefix.Length..];
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path = string.Empty;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string BuildDefaultLogFilePath(string? ebootPath)
|
||||||
|
{
|
||||||
|
var baseDirectory = AppContext.BaseDirectory;
|
||||||
|
var logsDirectory = Path.Combine(baseDirectory, "user", "logs");
|
||||||
|
var name = TryReadTitleId(ebootPath) ?? "UNKNOWN";
|
||||||
|
|
||||||
|
foreach (var invalid in Path.GetInvalidFileNameChars())
|
||||||
|
{
|
||||||
|
name = name.Replace(invalid, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(logsDirectory, $"{name}-{DateTime.Now:yyyyMMdd-HHmmss}.log");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TryReadTitleId(string? ebootPath)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(ebootPath))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(Path.GetFullPath(ebootPath));
|
||||||
|
if (string.IsNullOrEmpty(directory))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var paramPath in new[]
|
||||||
|
{
|
||||||
|
Path.Combine(directory, "sce_sys", "param.json"),
|
||||||
|
Path.Combine(directory, "param.json"),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (!File.Exists(paramPath))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
using var stream = File.OpenRead(paramPath);
|
||||||
|
using var document = JsonDocument.Parse(stream);
|
||||||
|
if (document.RootElement.TryGetProperty("titleId", out var titleIdElement) &&
|
||||||
|
titleIdElement.ValueKind == JsonValueKind.String)
|
||||||
|
{
|
||||||
|
var titleId = titleIdElement.GetString();
|
||||||
|
if (!string.IsNullOrWhiteSpace(titleId))
|
||||||
|
{
|
||||||
|
return titleId.Trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// Logging should never block launch; unknown title ids use a stable fallback.
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? TryFindEbootPathToken(IReadOnlyList<string> args)
|
||||||
|
{
|
||||||
|
for (var i = args.Count - 1; i >= 0; i--)
|
||||||
|
{
|
||||||
|
var argument = args[i];
|
||||||
|
if (string.IsNullOrWhiteSpace(argument) ||
|
||||||
|
argument.StartsWith("--", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
return argument;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ShouldConsumeLogFilePath(IReadOnlyList<string> args, int candidateIndex)
|
||||||
|
{
|
||||||
|
var candidate = args[candidateIndex];
|
||||||
|
if (LooksLikeLogFilePath(candidate))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var i = candidateIndex + 1; i < args.Count; i++)
|
||||||
|
{
|
||||||
|
var argument = args[i];
|
||||||
|
if (!string.IsNullOrWhiteSpace(argument) &&
|
||||||
|
!argument.StartsWith("--", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool LooksLikeLogFilePath(string path)
|
||||||
|
{
|
||||||
|
var extension = Path.GetExtension(path);
|
||||||
|
return string.Equals(extension, ".log", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
string.Equals(extension, ".txt", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TryEnableConsoleFileMirror(string path)
|
||||||
|
{
|
||||||
|
lock (ConsoleMirrorSync)
|
||||||
|
{
|
||||||
|
if (_consoleMirrorFile is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var directory = Path.GetDirectoryName(path);
|
||||||
|
if (!string.IsNullOrEmpty(directory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(directory);
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = new FileStream(
|
||||||
|
path,
|
||||||
|
FileMode.Create,
|
||||||
|
FileAccess.Write,
|
||||||
|
FileShare.ReadWrite,
|
||||||
|
bufferSize: 4096,
|
||||||
|
FileOptions.SequentialScan);
|
||||||
|
_consoleMirrorFile = new StreamWriter(stream, new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
|
||||||
|
{
|
||||||
|
AutoFlush = true,
|
||||||
|
};
|
||||||
|
|
||||||
|
Console.SetOut(new TeeTextWriter(Console.Out, _consoleMirrorFile));
|
||||||
|
Console.SetError(new TeeTextWriter(Console.Error, _consoleMirrorFile));
|
||||||
|
Console.Error.WriteLine($"[DEBUG] Log file: {Path.GetFullPath(path)}");
|
||||||
|
}
|
||||||
|
catch (Exception ex)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[WARN] Could not open log file '{path}': {ex.Message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void DropConsoleFileMirror()
|
||||||
|
{
|
||||||
|
lock (ConsoleMirrorSync)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_consoleMirrorFile?.Flush();
|
||||||
|
_consoleMirrorFile?.Dispose();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
_consoleMirrorFile = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ConfigureInheritedStdHandles(ref STARTUPINFO startupInfo)
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var input = GetStdHandle(STD_INPUT_HANDLE);
|
||||||
|
var output = GetStdHandle(STD_OUTPUT_HANDLE);
|
||||||
|
var error = GetStdHandle(STD_ERROR_HANDLE);
|
||||||
|
if (!IsHandleValid(output) && !IsHandleValid(error))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsHandleValid(input))
|
||||||
|
{
|
||||||
|
_ = SetHandleInformation(input, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
|
||||||
|
startupInfo.hStdInput = input;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsHandleValid(output))
|
||||||
|
{
|
||||||
|
_ = SetHandleInformation(output, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
|
||||||
|
startupInfo.hStdOutput = output;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (IsHandleValid(error))
|
||||||
|
{
|
||||||
|
_ = SetHandleInformation(error, HANDLE_FLAG_INHERIT, HANDLE_FLAG_INHERIT);
|
||||||
|
startupInfo.hStdError = error;
|
||||||
|
}
|
||||||
|
|
||||||
|
startupInfo.dwFlags |= STARTF_USESTDHANDLES;
|
||||||
|
}
|
||||||
|
|
||||||
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
|
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
|
||||||
{
|
{
|
||||||
var builder = new StringBuilder();
|
var builder = new StringBuilder();
|
||||||
@@ -458,27 +768,30 @@ internal static partial class Program
|
|||||||
|
|
||||||
private static void PrintUsage()
|
private static void PrintUsage()
|
||||||
{
|
{
|
||||||
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] <path-to-eboot.bin>");
|
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] [--log-file[=<path>]] <path-to-eboot.bin>");
|
||||||
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug ""E:\Games\...\eboot.bin""");
|
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryParseArguments(
|
private static bool TryParseArguments(
|
||||||
string[] args,
|
string[] args,
|
||||||
out string ebootPath,
|
out string ebootPath,
|
||||||
out SharpEmuRuntimeOptions runtimeOptions,
|
out SharpEmuRuntimeOptions runtimeOptions,
|
||||||
out LogLevel logLevel)
|
out LogLevel logLevel,
|
||||||
|
out string? logFilePath)
|
||||||
{
|
{
|
||||||
if (args.Length == 0)
|
if (args.Length == 0)
|
||||||
{
|
{
|
||||||
ebootPath = string.Empty;
|
ebootPath = string.Empty;
|
||||||
runtimeOptions = default;
|
runtimeOptions = default;
|
||||||
logLevel = SharpEmuLog.MinimumLevel;
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
|
logFilePath = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var strictDynlibResolution = false;
|
var strictDynlibResolution = false;
|
||||||
var importTraceLimit = 0;
|
var importTraceLimit = 0;
|
||||||
var cpuEngine = CpuExecutionEngine.NativeOnly;
|
var cpuEngine = CpuExecutionEngine.NativeOnly;
|
||||||
|
logFilePath = null;
|
||||||
logLevel = SharpEmuLog.MinimumLevel;
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
var pathTokens = new List<string>(args.Length);
|
var pathTokens = new List<string>(args.Length);
|
||||||
for (var i = 0; i < args.Length; i++)
|
for (var i = 0; i < args.Length; i++)
|
||||||
@@ -508,6 +821,7 @@ internal static partial class Program
|
|||||||
{
|
{
|
||||||
ebootPath = string.Empty;
|
ebootPath = string.Empty;
|
||||||
runtimeOptions = default;
|
runtimeOptions = default;
|
||||||
|
logFilePath = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -521,6 +835,7 @@ internal static partial class Program
|
|||||||
{
|
{
|
||||||
ebootPath = string.Empty;
|
ebootPath = string.Empty;
|
||||||
runtimeOptions = default;
|
runtimeOptions = default;
|
||||||
|
logFilePath = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -528,6 +843,23 @@ internal static partial class Program
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (string.Equals(argument, "--log-file", StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
if (i + 1 < args.Length &&
|
||||||
|
!string.IsNullOrWhiteSpace(args[i + 1]) &&
|
||||||
|
!args[i + 1].StartsWith("--", StringComparison.Ordinal) &&
|
||||||
|
ShouldConsumeLogFilePath(args, i + 1))
|
||||||
|
{
|
||||||
|
logFilePath = args[++i];
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
logFilePath = BuildDefaultLogFilePath(TryFindEbootPathToken(args));
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
const string logLevelPrefix = "--log-level=";
|
const string logLevelPrefix = "--log-level=";
|
||||||
if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase))
|
if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
{
|
{
|
||||||
@@ -551,6 +883,7 @@ internal static partial class Program
|
|||||||
ebootPath = string.Empty;
|
ebootPath = string.Empty;
|
||||||
runtimeOptions = default;
|
runtimeOptions = default;
|
||||||
logLevel = SharpEmuLog.MinimumLevel;
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
|
logFilePath = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -573,11 +906,27 @@ internal static partial class Program
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const string logFilePrefix = "--log-file=";
|
||||||
|
if (argument.StartsWith(logFilePrefix, StringComparison.OrdinalIgnoreCase))
|
||||||
|
{
|
||||||
|
logFilePath = argument[logFilePrefix.Length..];
|
||||||
|
if (string.IsNullOrWhiteSpace(logFilePath))
|
||||||
|
{
|
||||||
|
ebootPath = string.Empty;
|
||||||
|
runtimeOptions = default;
|
||||||
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
if (argument.StartsWith("--", StringComparison.Ordinal))
|
if (argument.StartsWith("--", StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
ebootPath = string.Empty;
|
ebootPath = string.Empty;
|
||||||
runtimeOptions = default;
|
runtimeOptions = default;
|
||||||
logLevel = SharpEmuLog.MinimumLevel;
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
|
logFilePath = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -589,6 +938,7 @@ internal static partial class Program
|
|||||||
ebootPath = string.Empty;
|
ebootPath = string.Empty;
|
||||||
runtimeOptions = default;
|
runtimeOptions = default;
|
||||||
logLevel = SharpEmuLog.MinimumLevel;
|
logLevel = SharpEmuLog.MinimumLevel;
|
||||||
|
logFilePath = null;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -690,6 +1040,56 @@ internal static partial class Program
|
|||||||
public nuint PeakJobMemoryUsed;
|
public nuint PeakJobMemoryUsed;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class TeeTextWriter : TextWriter
|
||||||
|
{
|
||||||
|
private readonly TextWriter _primary;
|
||||||
|
private readonly TextWriter _mirror;
|
||||||
|
|
||||||
|
public TeeTextWriter(TextWriter primary, TextWriter mirror)
|
||||||
|
{
|
||||||
|
_primary = primary;
|
||||||
|
_mirror = mirror;
|
||||||
|
}
|
||||||
|
|
||||||
|
public override Encoding Encoding => _primary.Encoding;
|
||||||
|
|
||||||
|
public override void Write(char value)
|
||||||
|
{
|
||||||
|
lock (ConsoleMirrorSync)
|
||||||
|
{
|
||||||
|
_primary.Write(value);
|
||||||
|
_mirror.Write(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Write(string? value)
|
||||||
|
{
|
||||||
|
lock (ConsoleMirrorSync)
|
||||||
|
{
|
||||||
|
_primary.Write(value);
|
||||||
|
_mirror.Write(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void WriteLine(string? value)
|
||||||
|
{
|
||||||
|
lock (ConsoleMirrorSync)
|
||||||
|
{
|
||||||
|
_primary.WriteLine(value);
|
||||||
|
_mirror.WriteLine(value);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public override void Flush()
|
||||||
|
{
|
||||||
|
lock (ConsoleMirrorSync)
|
||||||
|
{
|
||||||
|
_primary.Flush();
|
||||||
|
_mirror.Flush();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
[DllImport("kernel32.dll", SetLastError = true)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool InitializeProcThreadAttributeList(
|
private static extern bool InitializeProcThreadAttributeList(
|
||||||
@@ -774,6 +1174,10 @@ internal static partial class Program
|
|||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool SetStdHandle(int stdHandle, nint handle);
|
private static extern bool SetStdHandle(int stdHandle, nint handle);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool SetHandleInformation(nint handle, uint mask, uint flags);
|
||||||
|
|
||||||
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
|
[DllImport("kernel32.dll", EntryPoint = "CreateFileW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||||
private static extern nint CreateFileW(
|
private static extern nint CreateFileW(
|
||||||
string fileName,
|
string fileName,
|
||||||
|
|||||||
@@ -4,9 +4,9 @@
|
|||||||
"net10.0": {
|
"net10.0": {
|
||||||
"Microsoft.NET.ILLink.Tasks": {
|
"Microsoft.NET.ILLink.Tasks": {
|
||||||
"type": "Direct",
|
"type": "Direct",
|
||||||
"requested": "[10.0.9, )",
|
"requested": "[10.0.3, )",
|
||||||
"resolved": "10.0.9",
|
"resolved": "10.0.3",
|
||||||
"contentHash": "4Iw41e2h7I4t70SJcX2GCmbyKJIlA273Cfm9RJMM050/3VBejGAG1KcthP5Z2L6SQcbfbf6BhNWO26+ZG+GzMg=="
|
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||||
},
|
},
|
||||||
"Avalonia.Angle.Windows.Natives": {
|
"Avalonia.Angle.Windows.Natives": {
|
||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
@@ -211,11 +211,15 @@
|
|||||||
"Avalonia.Desktop": "[11.3.18, )",
|
"Avalonia.Desktop": "[11.3.18, )",
|
||||||
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
||||||
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
||||||
|
"SharpEmu.Logging": "[1.0.0, )",
|
||||||
"Tmds.DBus.Protocol": "[0.21.3, )"
|
"Tmds.DBus.Protocol": "[0.21.3, )"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"sharpemu.hle": {
|
"sharpemu.hle": {
|
||||||
"type": "Project"
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.Logging": "[1.0.0, )"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sharpemu.libs": {
|
"sharpemu.libs": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
@@ -484,4 +488,4 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
|
|
||||||
@@ -37,13 +38,78 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RecordDeferredBootstrapTrace(
|
||||||
|
long dispatchIndex,
|
||||||
|
ulong op,
|
||||||
|
ulong symbolPointer,
|
||||||
|
ulong outputPointer,
|
||||||
|
ulong returnRip)
|
||||||
|
{
|
||||||
|
lock (_deferredBootstrapTraceGate)
|
||||||
|
{
|
||||||
|
_deferredBootstrapTrace[_deferredBootstrapTraceWriteIndex] = new DeferredBootstrapTraceEntry(
|
||||||
|
dispatchIndex,
|
||||||
|
op,
|
||||||
|
symbolPointer,
|
||||||
|
outputPointer,
|
||||||
|
returnRip);
|
||||||
|
_deferredBootstrapTraceWriteIndex =
|
||||||
|
(_deferredBootstrapTraceWriteIndex + 1) % _deferredBootstrapTrace.Length;
|
||||||
|
if (_deferredBootstrapTraceCount < _deferredBootstrapTrace.Length)
|
||||||
|
{
|
||||||
|
_deferredBootstrapTraceCount++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DrainDeferredBootstrapTraces()
|
||||||
|
{
|
||||||
|
if (!_logBootstrap)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DeferredBootstrapTraceEntry[] pending;
|
||||||
|
lock (_deferredBootstrapTraceGate)
|
||||||
|
{
|
||||||
|
if (_deferredBootstrapTraceCount == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
pending = new DeferredBootstrapTraceEntry[_deferredBootstrapTraceCount];
|
||||||
|
var readIndex = (_deferredBootstrapTraceWriteIndex - _deferredBootstrapTraceCount +
|
||||||
|
_deferredBootstrapTrace.Length) % _deferredBootstrapTrace.Length;
|
||||||
|
for (var i = 0; i < _deferredBootstrapTraceCount; i++)
|
||||||
|
{
|
||||||
|
pending[i] = _deferredBootstrapTrace[(readIndex + i) % _deferredBootstrapTrace.Length];
|
||||||
|
}
|
||||||
|
|
||||||
|
_deferredBootstrapTraceCount = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var entry in pending)
|
||||||
|
{
|
||||||
|
var symbolText = "<unreadable>";
|
||||||
|
if (TryReadAsciiZ(entry.SymbolPointer, 256, out var sym))
|
||||||
|
{
|
||||||
|
symbolText = sym;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] bootstrap_call#{entry.DispatchIndex}: op=0x{entry.Op:X16} " +
|
||||||
|
$"sym_ptr=0x{entry.SymbolPointer:X16} sym='{symbolText}' " +
|
||||||
|
$"out_ptr=0x{entry.OutputPointer:X16} ret=0x{entry.ReturnRip:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void DumpRecentImportTrace()
|
private void DumpRecentImportTrace()
|
||||||
{
|
{
|
||||||
if (_recentImportTraceCount == 0)
|
if (_recentImportTraceCount == 0)
|
||||||
{
|
{
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Recent import calls ({_recentImportTraceCount}):");
|
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
|
||||||
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
|
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
|
||||||
for (int i = 0; i < _recentImportTraceCount; i++)
|
for (int i = 0; i < _recentImportTraceCount; i++)
|
||||||
{
|
{
|
||||||
@@ -51,8 +117,8 @@ public sealed partial class DirectExecutionBackend
|
|||||||
var entry = _recentImportTrace[num2];
|
var entry = _recentImportTrace[num2];
|
||||||
if (!string.IsNullOrEmpty(entry.Nid))
|
if (!string.IsNullOrEmpty(entry.Nid))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[LOADER][INFO] #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
$" #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||||
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
|
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -91,12 +157,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
list.Add(num7);
|
list.Add(num7);
|
||||||
if (num2 < 32)
|
if (num2 < 32)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
|
Log.Info($"Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
|
||||||
num2++;
|
num2++;
|
||||||
}
|
}
|
||||||
if (num >= 16384)
|
if (num >= 16384)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
|
Log.Warning($"Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -106,7 +172,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
}
|
}
|
||||||
if (num != 0)
|
if (num != 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer hits: {num}");
|
Log.Warning($"Suspicious unresolved pointer hits: {num}");
|
||||||
}
|
}
|
||||||
return list;
|
return list;
|
||||||
}
|
}
|
||||||
@@ -121,18 +187,18 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Span<byte> destination = stackalloc byte[128];
|
Span<byte> destination = stackalloc byte[128];
|
||||||
if (!cpuContext.Memory.TryRead(returnRip, destination))
|
if (!cpuContext.Memory.TryRead(returnRip, destination))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
|
Log.Debug($"Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
string value = BitConverter.ToString(destination.ToArray()).Replace("-", " ");
|
string value = BitConverter.ToString(destination.ToArray()).Replace("-", " ");
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
|
Log.Debug($"Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
|
||||||
if (destination[0] == byte.MaxValue && (destination[1] == 21 || destination[1] == 37))
|
if (destination[0] == byte.MaxValue && (destination[1] == 21 || destination[1] == 37))
|
||||||
{
|
{
|
||||||
int num = BitConverter.ToInt32(destination.Slice(2, 4));
|
int num = BitConverter.ToInt32(destination.Slice(2, 4));
|
||||||
ulong num2 = returnRip + 6 + (ulong)num;
|
ulong num2 = returnRip + 6 + (ulong)num;
|
||||||
if (cpuContext.TryReadUInt64(num2, out var value2))
|
if (cpuContext.TryReadUInt64(num2, out var value2))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
|
Log.Debug($"Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (destination[0] == 72 && destination[1] == 139 && destination[2] == 5)
|
if (destination[0] == 72 && destination[1] == 139 && destination[2] == 5)
|
||||||
@@ -141,7 +207,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong num4 = returnRip + 7 + (ulong)num3;
|
ulong num4 = returnRip + 7 + (ulong)num3;
|
||||||
if (cpuContext.TryReadUInt64(num4, out var value3))
|
if (cpuContext.TryReadUInt64(num4, out var value3))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
|
Log.Debug($"Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (int i = 0; i + 6 <= destination.Length; i++)
|
for (int i = 0; i + 6 <= destination.Length; i++)
|
||||||
@@ -153,7 +219,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong num7 = num6 + 6 + (ulong)num5;
|
ulong num7 = num6 + 6 + (ulong)num5;
|
||||||
if (cpuContext.TryReadUInt64(num7, out var value4))
|
if (cpuContext.TryReadUInt64(num7, out var value4))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
|
Log.Debug($"Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -168,32 +234,33 @@ public sealed partial class DirectExecutionBackend
|
|||||||
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
|
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
|
||||||
ulong callRip = returnRip + (ulong)i;
|
ulong callRip = returnRip + (ulong)i;
|
||||||
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
||||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
var importEntries = _importEntries;
|
||||||
|
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
||||||
{
|
{
|
||||||
if (_importEntries[importIndex].Address != target)
|
if (importEntries[importIndex].Address != target)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
string nid = _importEntries[importIndex].Nid;
|
string nid = importEntries[importIndex].Nid;
|
||||||
if (_moduleManager.TryGetExport(nid, out var export))
|
if (_moduleManager.TryGetExport(nid, out var export))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
$"Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
$"Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cpuContext.Memory.TryRead(target, targetBytes))
|
if (cpuContext.Memory.TryRead(target, targetBytes))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
$"Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
||||||
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
|
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
|
||||||
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
|
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
|
||||||
{
|
{
|
||||||
@@ -201,25 +268,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
|
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
|
||||||
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
|
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
||||||
{
|
{
|
||||||
if (_importEntries[importIndex].Address != slotTarget)
|
if (importEntries[importIndex].Address != slotTarget)
|
||||||
{
|
{
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
string nid = _importEntries[importIndex].Nid;
|
string nid = importEntries[importIndex].Nid;
|
||||||
if (_moduleManager.TryGetExport(nid, out var export))
|
if (_moduleManager.TryGetExport(nid, out var export))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
||||||
}
|
}
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -103,6 +103,15 @@ public sealed partial class DirectExecutionBackend
|
|||||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||||
|
|
||||||
|
// Thread-mode probe: a hardware exception raised while this thread is inside
|
||||||
|
// the managed import gateway means the VEH->managed reentry happened from
|
||||||
|
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
|
||||||
|
if (LogThreadMode && _threadModeGatewayDepth > 0)
|
||||||
|
{
|
||||||
|
TraceThreadMode(
|
||||||
|
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
||||||
|
}
|
||||||
|
|
||||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||||
{
|
{
|
||||||
return -1;
|
return -1;
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ using System.Linq;
|
|||||||
using System.Runtime.InteropServices;
|
using System.Runtime.InteropServices;
|
||||||
using System.Threading;
|
using System.Threading;
|
||||||
using SharpEmu.Core.Cpu;
|
using SharpEmu.Core.Cpu;
|
||||||
|
using SharpEmu.Core.Loader;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Cpu.Native;
|
namespace SharpEmu.Core.Cpu.Native;
|
||||||
@@ -19,6 +20,16 @@ public sealed partial class DirectExecutionBackend
|
|||||||
|
|
||||||
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
|
private static ulong ImportDispatchGatewayManaged(nint backendHandle, int importIndex, nint argPackPtr)
|
||||||
{
|
{
|
||||||
|
if (LogThreadMode)
|
||||||
|
{
|
||||||
|
_threadModeGatewayCalls++;
|
||||||
|
_threadModeGatewayDepth++;
|
||||||
|
if (!_threadModeGatewayFirstLogged)
|
||||||
|
{
|
||||||
|
_threadModeGatewayFirstLogged = true;
|
||||||
|
TraceThreadMode($"gateway_first import={importIndex} total={_threadModeGatewayCalls}");
|
||||||
|
}
|
||||||
|
}
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
|
if (!(GCHandle.FromIntPtr(backendHandle).Target is DirectExecutionBackend directExecutionBackend))
|
||||||
@@ -36,6 +47,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
|
$"[LOADER][ERROR] ImportDispatchGatewayManaged exception: {ex.GetType().Name}: {ex.Message}");
|
||||||
return 18446744071562199298uL;
|
return 18446744071562199298uL;
|
||||||
}
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
if (LogThreadMode)
|
||||||
|
{
|
||||||
|
_threadModeGatewayDepth--;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
|
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
|
||||||
@@ -69,6 +87,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
WriteCtxU64(contextRecord, 152, nextRsp);
|
WriteCtxU64(contextRecord, 152, nextRsp);
|
||||||
WriteCtxU64(contextRecord, 248, returnRip);
|
WriteCtxU64(contextRecord, 248, returnRip);
|
||||||
Interlocked.Increment(ref _rawSentinelRecoveries);
|
Interlocked.Increment(ref _rawSentinelRecoveries);
|
||||||
|
if (LogThreadMode)
|
||||||
|
{
|
||||||
|
TraceThreadMode(
|
||||||
|
$"sentinel_recover rip=0x{value:X16} -> 0x{returnRip:X16} rsp=0x{rsp:X16} -> 0x{nextRsp:X16} " +
|
||||||
|
$"gateway_depth={_threadModeGatewayDepth}");
|
||||||
|
}
|
||||||
return -1;
|
return -1;
|
||||||
}
|
}
|
||||||
return 0;
|
return 0;
|
||||||
@@ -87,18 +111,25 @@ public sealed partial class DirectExecutionBackend
|
|||||||
LastError = "Import dispatch called without active CPU context";
|
LastError = "Import dispatch called without active CPU context";
|
||||||
return 18446744071562199298uL;
|
return 18446744071562199298uL;
|
||||||
}
|
}
|
||||||
if ((uint)importIndex >= (uint)_importEntries.Length)
|
var importEntries = _importEntries;
|
||||||
|
if ((uint)importIndex >= (uint)importEntries.Length)
|
||||||
{
|
{
|
||||||
LastError = $"Import dispatch index out of range: {importIndex}";
|
LastError = $"Import dispatch index out of range: {importIndex}";
|
||||||
return 18446744071562199042uL;
|
return 18446744071562199042uL;
|
||||||
}
|
}
|
||||||
ImportStubEntry importStubEntry = _importEntries[importIndex];
|
ImportStubEntry importStubEntry = importEntries[importIndex];
|
||||||
int num2 = Volatile.Read(in _rawSentinelRecoveries);
|
int num2 = Volatile.Read(in _rawSentinelRecoveries);
|
||||||
if (num2 != _lastReportedRawSentinelRecoveries)
|
if (num2 != _lastReportedRawSentinelRecoveries)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
|
Console.Error.WriteLine($"[LOADER][TRACE] Raw sentinel recoveries: {num2} (last import index={importIndex})");
|
||||||
_lastReportedRawSentinelRecoveries = num2;
|
_lastReportedRawSentinelRecoveries = num2;
|
||||||
}
|
}
|
||||||
|
// Leaf imports take a scalar-only fast path that reads its own operands and
|
||||||
|
// intentionally bypasses the SysV variadic XMM marshalling below. This is safe
|
||||||
|
// only while the leaf set contains no float-variadic or float-returning function.
|
||||||
|
// The constraint and the audited list live on IsLeafImport — do not add a
|
||||||
|
// function that consumes xmm0-7 args or returns in xmm0 to the leaf set;
|
||||||
|
// such functions must stay on the full gateway path below.
|
||||||
if (IsLeafImport(importStubEntry.Nid) &&
|
if (IsLeafImport(importStubEntry.Nid) &&
|
||||||
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
|
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
|
||||||
{
|
{
|
||||||
@@ -118,11 +149,26 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
|
cpuContext[CpuRegister.R13] = *(ulong*)(argPackPtr + 72);
|
||||||
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
|
||||||
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
|
||||||
cpuContext.SetXmmRegister(
|
// The trampoline spills the SysV variadic XMM save area (xmm0..xmm7) into the
|
||||||
0,
|
// 0x80 bytes immediately below the GP argpack, so variadic float args (printf
|
||||||
*(ulong*)(argPackPtr - 16),
|
// %f, and powf/logf inputs) reach the handler. xmm{i} is at argPackPtr-0x80+i*16.
|
||||||
*(ulong*)(argPackPtr - 8));
|
for (var xmmIndex = 0; xmmIndex < 8; xmmIndex++)
|
||||||
|
{
|
||||||
|
var xmmSlot = argPackPtr - 0x80 + (xmmIndex * 16);
|
||||||
|
cpuContext.SetXmmRegister(
|
||||||
|
xmmIndex,
|
||||||
|
*(ulong*)xmmSlot,
|
||||||
|
*(ulong*)(xmmSlot + 8));
|
||||||
|
}
|
||||||
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
|
||||||
|
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
NormalizeKernelDynlibDlsymArguments(cpuContext, out _, out _);
|
||||||
|
*(ulong*)argPackPtr = cpuContext[CpuRegister.Rdi];
|
||||||
|
*(ulong*)(argPackPtr + 8) = cpuContext[CpuRegister.Rsi];
|
||||||
|
*(ulong*)(argPackPtr + 16) = cpuContext[CpuRegister.Rdx];
|
||||||
|
}
|
||||||
|
|
||||||
ulong value = cpuContext[CpuRegister.Rdi];
|
ulong value = cpuContext[CpuRegister.Rdi];
|
||||||
ulong value2 = cpuContext[CpuRegister.Rsi];
|
ulong value2 = cpuContext[CpuRegister.Rsi];
|
||||||
ulong num3 = cpuContext[CpuRegister.Rdx];
|
ulong num3 = cpuContext[CpuRegister.Rdx];
|
||||||
@@ -173,16 +219,6 @@ public sealed partial class DirectExecutionBackend
|
|||||||
TraceGuestContext(
|
TraceGuestContext(
|
||||||
$"import dispatch={num} nid={importStubEntry.Nid} ret=0x{num7:X16} managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16} active={HasActiveExecutionThread}");
|
$"import dispatch={num} nid={importStubEntry.Nid} ret=0x{num7:X16} managed={Environment.CurrentManagedThreadId} guest=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} fiber=0x{GuestThreadExecution.CurrentFiberAddress:X16} active={HasActiveExecutionThread}");
|
||||||
}
|
}
|
||||||
if (_logBootstrap && string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
|
||||||
{
|
|
||||||
string symbolText = "<unreadable>";
|
|
||||||
if (TryReadAsciiZ(value2, 256, out var sym))
|
|
||||||
{
|
|
||||||
symbolText = sym;
|
|
||||||
}
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
|
|
||||||
}
|
|
||||||
if (!isGuestWorker &&
|
if (!isGuestWorker &&
|
||||||
!ActiveForcedGuestExit &&
|
!ActiveForcedGuestExit &&
|
||||||
ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) &&
|
ShouldForceGuestExitOnImportLoop(importStubEntry.Nid, num7, num, value, value2) &&
|
||||||
@@ -191,6 +227,17 @@ public sealed partial class DirectExecutionBackend
|
|||||||
cpuContext[CpuRegister.Rax] = 1uL;
|
cpuContext[CpuRegister.Rax] = 1uL;
|
||||||
return 1uL;
|
return 1uL;
|
||||||
}
|
}
|
||||||
|
// Backend teardown in progress: unwind this worker to the host instead of
|
||||||
|
// dispatching. RunGuestThread parks it as Blocked with no wake handler, its
|
||||||
|
// host thread exits, and RequestGuestThreadTeardown can finish before any
|
||||||
|
// executable memory is freed.
|
||||||
|
if (isGuestWorker &&
|
||||||
|
_guestTeardownRequested &&
|
||||||
|
TryYieldGuestThreadToHostStub(argPackPtr, num, num7, importStubEntry.Nid, "backend teardown"))
|
||||||
|
{
|
||||||
|
cpuContext[CpuRegister.Rax] = 0uL;
|
||||||
|
return 0uL;
|
||||||
|
}
|
||||||
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
bool flag0 = ShouldSuppressStrlenTrace(importStubEntry.Nid);
|
||||||
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
|
||||||
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
bool flag2 = num7 >= 2156351360u && num7 <= 2156352080u;
|
||||||
@@ -336,6 +383,11 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
if (string.Equals(importStubEntry.Nid, RuntimeStubNids.BootstrapBridge, StringComparison.Ordinal))
|
||||||
{
|
{
|
||||||
|
if (_logBootstrap)
|
||||||
|
{
|
||||||
|
RecordDeferredBootstrapTrace(num, value, value2, num3, num7);
|
||||||
|
}
|
||||||
|
|
||||||
orbisGen2Result = DispatchBootstrapBridge();
|
orbisGen2Result = DispatchBootstrapBridge();
|
||||||
}
|
}
|
||||||
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
|
else if (string.Equals(importStubEntry.Nid, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal) ||
|
||||||
@@ -491,6 +543,14 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.Flush();
|
Console.Error.Flush();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
// Publish the handler's XMM0 back into the argpack's xmm0 save slot; the
|
||||||
|
// trampoline epilogue reloads it into the guest's XMM0, delivering float/double
|
||||||
|
// return values (powf/logf/wcstod). Harmless for int/pointer returns (XMM is
|
||||||
|
// volatile across a SysV call, so the guest never relies on a preserved XMM0).
|
||||||
|
cpuContext.GetXmmRegister(0, out var returnXmm0Low, out var returnXmm0High);
|
||||||
|
*(ulong*)(argPackPtr - 0x80) = returnXmm0Low;
|
||||||
|
*(ulong*)(argPackPtr - 0x80 + 8) = returnXmm0High;
|
||||||
|
DrainDeferredBootstrapTraces();
|
||||||
return cpuContext[CpuRegister.Rax];
|
return cpuContext[CpuRegister.Rax];
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
@@ -499,6 +559,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
Console.Error.WriteLine($"[LOADER][ERROR] {LastError}");
|
Console.Error.WriteLine($"[LOADER][ERROR] {LastError}");
|
||||||
Console.Error.WriteLine($"[LOADER][ERROR] {ex.StackTrace}");
|
Console.Error.WriteLine($"[LOADER][ERROR] {ex.StackTrace}");
|
||||||
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
cpuContext[CpuRegister.Rax] = 18446744071562199298uL;
|
||||||
|
DrainDeferredBootstrapTraces();
|
||||||
return 18446744071562199298uL;
|
return 18446744071562199298uL;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -622,6 +683,13 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Subset of the leaf set that additionally skips the import-call-frame
|
||||||
|
// bookkeeping. The same scalar-only (no XMM args, no XMM return) constraint
|
||||||
|
// as IsLeafImport applies — see the note there before adding entries.
|
||||||
|
// NOTE: this filter is only consulted after IsLeafImport accepts the NID;
|
||||||
|
// entries listed here but not in IsLeafImport (scePadRead, scePadOpen,
|
||||||
|
// sceUserServiceGetEvent, sceUserServiceGetPlatformPrivacySetting,
|
||||||
|
// pthread_mutex_trylock) currently take the full gateway path.
|
||||||
private static bool IsNoBlockLeafImport(string nid) =>
|
private static bool IsNoBlockLeafImport(string nid) =>
|
||||||
nid is
|
nid is
|
||||||
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||||
@@ -644,11 +712,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||||
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||||
"Q2V+iqvjgC0" or // vsnprintf
|
"Q2V+iqvjgC0" or // vsnprintf
|
||||||
|
"AV6ipCNa4Rw" or // strcasecmp
|
||||||
"q1cHNfGycLI" or // scePadRead
|
"q1cHNfGycLI" or // scePadRead
|
||||||
"xk0AcarP3V4" or // scePadOpen
|
"xk0AcarP3V4" or // scePadOpen
|
||||||
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
"yH17Q6NWtVg" or // sceUserServiceGetEvent
|
||||||
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
|
"D-CzAxQL0XI" or // sceUserServiceGetPlatformPrivacySetting
|
||||||
"K-jXhbt2gn4"; // scePthreadMutexTrylock
|
"K-jXhbt2gn4"; // pthread_mutex_trylock (scePthreadMutexTrylock is upoVrzMHFeE)
|
||||||
|
|
||||||
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
private bool ShouldLogImportResult(string nid, OrbisGen2Result result)
|
||||||
{
|
{
|
||||||
@@ -715,22 +784,44 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"1G3lF1Gg1k8" or // sceKernelOpen
|
"1G3lF1Gg1k8" or // sceKernelOpen
|
||||||
"gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes
|
"gEpBkcwxUjw"; // sceKernelAprResolveFilepathsToIdsAndFileSizes
|
||||||
|
|
||||||
|
// LEAF-IMPORT REGISTRATION — scalar-only constraint.
|
||||||
|
//
|
||||||
|
// TryDispatchLeafImport is a fast path that never copies the trampoline's XMM
|
||||||
|
// save area into CpuContext and never publishes an XMM0 return back to the
|
||||||
|
// guest (see DispatchImport). Every NID listed here must therefore satisfy BOTH:
|
||||||
|
// 1. no float/double parameters (nothing passed in xmm0..xmm7), and
|
||||||
|
// 2. no float/double return value (nothing returned in xmm0).
|
||||||
|
// Integer/pointer arguments and returns only. va_list-based functions
|
||||||
|
// (vsnprintf) are safe: SysV va_list floats are read from the caller-built
|
||||||
|
// reg_save_area in guest memory, not from the callee's incoming XMM registers.
|
||||||
|
//
|
||||||
|
// If a function that consumes XMM arguments or returns in xmm0 (e.g. powf,
|
||||||
|
// logf, wcstod, sceVideoOutColorSettingsSetGamma_) is ever added here, its
|
||||||
|
// float arguments will silently arrive stale and its return value will be
|
||||||
|
// dropped. Such functions must stay on the full gateway path — do not list them.
|
||||||
|
//
|
||||||
|
// Audited 2026-07-11 (PR #59): every NID below maps to a registered
|
||||||
|
// SysAbiExport whose handler neither reads nor writes CpuContext XMM state
|
||||||
|
// and whose known guest signature is integer/pointer only.
|
||||||
private bool IsLeafImport(string nid)
|
private bool IsLeafImport(string nid)
|
||||||
{
|
{
|
||||||
if (nid == "1jfXLRVzisc")
|
if (nid == "1jfXLRVzisc") // sceKernelUsleep
|
||||||
{
|
{
|
||||||
return !_logUsleep;
|
return !_logUsleep;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Only mutex/rwlock *lock* is excluded: it may block a contended acquire, which the
|
||||||
|
// leaf path can't. unlock never blocks and stays here — routing it off the fast path
|
||||||
|
// slows guest spinlocks enough to livelock (Demon's Souls).
|
||||||
return nid is
|
return nid is
|
||||||
"9UK1vLZQft4" or
|
"tn3VlD0hG60" or // scePthreadMutexUnlock
|
||||||
"tn3VlD0hG60" or
|
"2Z+PpY6CaJg" or // pthread_mutex_unlock
|
||||||
"7H0iTOciTLo" or
|
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
||||||
"2Z+PpY6CaJg" or
|
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
||||||
"8aI7R7WaOlc" or
|
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
|
||||||
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
|
||||||
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
|
||||||
"qj7QZpgr9Uw" or // Gen5 graphics type-2 packet
|
"qj7QZpgr9Uw" or // sceAgcUnknownQj7QZpgr9Uw (unknown NID; observed scalar: command-buffer pointer in, packet address out)
|
||||||
"LtTouSCZjHM" or // sceAgcCbNop
|
"LtTouSCZjHM" or // sceAgcCbNop
|
||||||
"k3GhuSNmBLU" or // sceAgcCbDispatch
|
"k3GhuSNmBLU" or // sceAgcCbDispatch
|
||||||
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
|
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
|
||||||
@@ -753,24 +844,24 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
|
"IxYiarKlXxM" or // sceAgcDmaDataPatchSetDstAddressOrOffset
|
||||||
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
|
"3KDcnM3lrcU" or // sceAgcWaitRegMemPatchAddress
|
||||||
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
|
"0fWWK5uG9rQ" or // sceAgcQueueEndOfPipeActionPatchAddress
|
||||||
"a8uLzYY--tM" or
|
"a8uLzYY--tM" or // sceAmprAprCommandBufferConstructor
|
||||||
"Qs1xtplKo0U" or
|
"Qs1xtplKo0U" or // sceAmprAprCommandBufferDestructor
|
||||||
"GuchCTefuZw" or
|
"GuchCTefuZw" or // sceAmprCommandBufferDestructor
|
||||||
"N-FSPA4S3nI" or
|
"N-FSPA4S3nI" or // sceAmprCommandBufferSetBuffer
|
||||||
"baQO9ez2gL4" or
|
"baQO9ez2gL4" or // sceAmprCommandBufferReset
|
||||||
"ULvXMDz56po" or
|
"ULvXMDz56po" or // sceAmprCommandBufferClearBuffer
|
||||||
"mQ16-QdKv7k" or
|
"mQ16-QdKv7k" or // sceAmprAprCommandBufferReadFile
|
||||||
"vWU-odnS+fU" or
|
"vWU-odnS+fU" or // sceAmprMeasureCommandSizeReadFile
|
||||||
"sSAUCCU1dv4" or
|
"sSAUCCU1dv4" or // sceAmprMeasureCommandSizeWriteKernelEventQueue_04_00
|
||||||
"C+IEj+BsAFM" or
|
"C+IEj+BsAFM" or // sceAmprMeasureCommandSizeWriteAddressOnCompletion
|
||||||
"tZDDEo2tE5k" or
|
"tZDDEo2tE5k" or // sceAmprCommandBufferGetSize
|
||||||
"GnxKOHEawhk" or
|
"GnxKOHEawhk" or // sceAmprCommandBufferGetCurrentOffset
|
||||||
"H896Pt-yB4I" or
|
"H896Pt-yB4I" or // sceAmprCommandBufferWriteKernelEventQueue_04_00
|
||||||
"sJXyWHjP-F8" or
|
"sJXyWHjP-F8" or // sceAmprCommandBufferWriteAddressOnCompletion
|
||||||
"ASoW5WE-UPo" or
|
"ASoW5WE-UPo" or // sceKernelAprSubmitCommandBufferAndGetResult
|
||||||
"rqwFKI4PAiM" or
|
"rqwFKI4PAiM" or // sceKernelAprWaitCommandBuffer
|
||||||
"eE4Szl8sil8" or
|
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
|
||||||
"qvMUCyyaCSI" or
|
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
|
||||||
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
|
"Vo5V8KAwCmk" or // sceSystemServiceHideSplashScreen
|
||||||
"TywrFKCoLGY" or // sceSaveDataInitialize3
|
"TywrFKCoLGY" or // sceSaveDataInitialize3
|
||||||
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
|
"dyIhnXq-0SM" or // sceSaveDataDirNameSearch
|
||||||
@@ -789,6 +880,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"WkkeywLJcgU" or // wcslen
|
"WkkeywLJcgU" or // wcslen
|
||||||
"Ovb2dSJOAuE" or // strcmp
|
"Ovb2dSJOAuE" or // strcmp
|
||||||
"aesyjrHVWy4" or // strncmp
|
"aesyjrHVWy4" or // strncmp
|
||||||
|
"AV6ipCNa4Rw" or // strcasecmp
|
||||||
"pNtJdE3x49E" or // wcscmp
|
"pNtJdE3x49E" or // wcscmp
|
||||||
"fV2xHER+bKE" or // wcscoll
|
"fV2xHER+bKE" or // wcscoll
|
||||||
"E8wCoUEbfzk" or // wcsncmp
|
"E8wCoUEbfzk" or // wcsncmp
|
||||||
@@ -799,12 +891,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"6ULAa0fq4jA" or // scePthreadRwlockInit
|
"6ULAa0fq4jA" or // scePthreadRwlockInit
|
||||||
"1471ajPzxh0" or // pthread_rwlock_destroy
|
"1471ajPzxh0" or // pthread_rwlock_destroy
|
||||||
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
|
"BB+kb08Tl9A" or // scePthreadRwlockDestroy
|
||||||
"iGjsr1WAtI0" or // pthread_rwlock_rdlock
|
// rwlock rd/wr lock removed (can block); init/destroy/unlock stay.
|
||||||
"Ox9i0c7L5w0" or // scePthreadRwlockRdlock
|
|
||||||
"sIlRvQqsN2Y" or // pthread_rwlock_wrlock
|
|
||||||
"mqdNorrB+gI" or // scePthreadRwlockWrlock
|
|
||||||
"EgmLo6EWgso" or // pthread_rwlock_unlock
|
|
||||||
"+L98PIbGttk" or // scePthreadRwlockUnlock
|
|
||||||
"aI+OeCz8xrQ" or // scePthreadSelf
|
"aI+OeCz8xrQ" or // scePthreadSelf
|
||||||
"EotR8a3ASf4" or // pthread_self
|
"EotR8a3ASf4" or // pthread_self
|
||||||
"eoht7mQOCmo" or // scePthreadGetspecific
|
"eoht7mQOCmo" or // scePthreadGetspecific
|
||||||
@@ -814,7 +901,12 @@ public sealed partial class DirectExecutionBackend
|
|||||||
"vz+pg2zdopI" or // sceKernelGetEventUserData
|
"vz+pg2zdopI" or // sceKernelGetEventUserData
|
||||||
"mJ7aghmgvfc" or // sceKernelGetEventId
|
"mJ7aghmgvfc" or // sceKernelGetEventId
|
||||||
"23CPPI1tyBY" or // sceKernelGetEventFilter
|
"23CPPI1tyBY" or // sceKernelGetEventFilter
|
||||||
"kwGyyjohI50"; // 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()
|
private long NextImportDispatchIndex()
|
||||||
@@ -1224,30 +1316,485 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
|
|
||||||
ulong outputAddress = cpuContext[CpuRegister.Rdx];
|
NormalizeKernelDynlibDlsymArguments(cpuContext, out var symbolNameAddress, out var outputAddress);
|
||||||
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName))
|
try
|
||||||
{
|
{
|
||||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
if (!TryReadAsciiZ(symbolNameAddress, 512, out var symbolName) ||
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
!TryResolveDlsymGuestAddress(symbolName, out var resolvedAddress) ||
|
||||||
|
outputAddress == 0 ||
|
||||||
|
!TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
||||||
|
{
|
||||||
|
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (!TryResolveRuntimeSymbolAddress(symbolName, out var resolvedAddress) &&
|
catch
|
||||||
!TryResolveRuntimeSymbolAlias(symbolName, out resolvedAddress))
|
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
|
||||||
$"[LOADER][WARN] sceKernelDlsym failed: handle=0x{cpuContext[CpuRegister.Rdi]:X} symbol='{symbolName}'");
|
|
||||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
if (outputAddress == 0L || !TryWriteUInt64Compat(outputAddress, resolvedAddress))
|
|
||||||
{
|
|
||||||
cpuContext[CpuRegister.Rax] = 18446744073709551615uL;
|
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cpuContext[CpuRegister.Rax] = 0uL;
|
cpuContext[CpuRegister.Rax] = 0uL;
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void NormalizeKernelDynlibDlsymArguments(
|
||||||
|
CpuContext cpuContext,
|
||||||
|
out ulong symbolNameAddress,
|
||||||
|
out ulong outputAddress)
|
||||||
|
{
|
||||||
|
var handle = cpuContext[CpuRegister.Rdi];
|
||||||
|
symbolNameAddress = cpuContext[CpuRegister.Rsi];
|
||||||
|
outputAddress = cpuContext[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
// Standalone bootstrap loaders sometimes call through the bridge with
|
||||||
|
// (symbol_ptr, handle, out) while sceKernelDlsym is (handle, symbol_ptr, out).
|
||||||
|
// Heuristic only: valid when RSI looks like a small handle and RDI is a guest pointer.
|
||||||
|
if (symbolNameAddress < 0x10000 &&
|
||||||
|
IsPlausibleDynlibSymbolPointer(handle))
|
||||||
|
{
|
||||||
|
symbolNameAddress = handle;
|
||||||
|
handle = cpuContext[CpuRegister.Rsi];
|
||||||
|
cpuContext[CpuRegister.Rdi] = handle;
|
||||||
|
cpuContext[CpuRegister.Rsi] = symbolNameAddress;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsPlausibleDynlibSymbolPointer(ulong address)
|
||||||
|
{
|
||||||
|
return address >= 0x10000 && address < 0x0000_8000_0000_0000UL;
|
||||||
|
}
|
||||||
|
|
||||||
|
private OrbisGen2Result CompleteKernelDynlibDlsymFailure(CpuContext cpuContext, ulong outputAddress)
|
||||||
|
{
|
||||||
|
if (outputAddress != 0)
|
||||||
|
{
|
||||||
|
_ = TryWriteUInt64Compat(outputAddress, 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
cpuContext[CpuRegister.Rax] = ulong.MaxValue;
|
||||||
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void ResetLazyDlsymStubState()
|
||||||
|
{
|
||||||
|
lock (_lazyDlsymStubGate)
|
||||||
|
{
|
||||||
|
_lazyDlsymStubCache.Clear();
|
||||||
|
_lazyImportStubPoolMapped = false;
|
||||||
|
_lazyImportStubPoolBase = 0;
|
||||||
|
_lazyImportStubNextSlot = 0;
|
||||||
|
_lazyImportStubPoolLimit = 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolveDlsymGuestAddress(string symbolName, out ulong guestAddress)
|
||||||
|
{
|
||||||
|
guestAddress = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(symbolName))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 0: ELF runtime symbols and aliases.
|
||||||
|
if (TryResolveRuntimeSymbolAddress(symbolName, out guestAddress) ||
|
||||||
|
TryResolveRuntimeSymbolAlias(symbolName, out guestAddress))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 1: existing import stub entries (duplicate prevention).
|
||||||
|
if (TryFindImportStubGuestAddress(symbolName, out guestAddress))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hasAerolibSymbol = Aerolib.Instance.TryGetByExportName(symbolName, out var hleSymbol);
|
||||||
|
if (hasAerolibSymbol)
|
||||||
|
{
|
||||||
|
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryResolveRuntimeSymbolAddress(hleSymbol.Nid, out guestAddress) ||
|
||||||
|
TryResolveRuntimeSymbolAddress(hleSymbol.ExportName, out guestAddress) ||
|
||||||
|
TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
|
||||||
|
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (Aerolib.Instance.TryGetByNid(symbolName, out hleSymbol))
|
||||||
|
{
|
||||||
|
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (TryFindImportStubGuestAddress(hleSymbol.Nid, out guestAddress) ||
|
||||||
|
TryFindImportStubGuestAddress(hleSymbol.ExportName, out guestAddress))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasAerolibSymbol = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Tier 3: lazy materialization for callable HLE symbols missing from ELF imports.
|
||||||
|
if (!TryResolveLazyDispatchTarget(symbolName, hasAerolibSymbol, in hleSymbol, out var dispatchNid, out var export))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return TryGetOrCreateLazyImportStub(
|
||||||
|
dispatchNid,
|
||||||
|
symbolName,
|
||||||
|
hasAerolibSymbol ? hleSymbol : null,
|
||||||
|
export,
|
||||||
|
out guestAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryFindImportStubGuestAddress(string identifier, out ulong guestAddress)
|
||||||
|
{
|
||||||
|
guestAddress = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(identifier))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var importEntries = _importEntries;
|
||||||
|
for (var i = 0; i < importEntries.Length; i++)
|
||||||
|
{
|
||||||
|
var entry = importEntries[i];
|
||||||
|
if (!ImportStubEntryMatchesIdentifier(entry, identifier))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Address >= 0x10000)
|
||||||
|
{
|
||||||
|
guestAddress = entry.Address;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ImportStubEntryMatchesIdentifier(in ImportStubEntry entry, string identifier)
|
||||||
|
{
|
||||||
|
if (string.Equals(entry.Nid, identifier, StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (entry.Export is not { } export)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return string.Equals(export.Name, identifier, StringComparison.Ordinal) ||
|
||||||
|
string.Equals(export.Nid, identifier, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsKernelDynlibDlsymIdentifier(string identifier)
|
||||||
|
{
|
||||||
|
return string.Equals(identifier, "sceKernelDlsym", StringComparison.Ordinal) ||
|
||||||
|
string.Equals(identifier, KernelDynlibDlsymAerolibNid, StringComparison.Ordinal) ||
|
||||||
|
string.Equals(identifier, RuntimeStubNids.KernelDynlibDlsym, StringComparison.Ordinal);
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryResolveLazyDispatchTarget(
|
||||||
|
string symbolName,
|
||||||
|
bool hasAerolibSymbol,
|
||||||
|
in SysAbiSymbol hleSymbol,
|
||||||
|
out string dispatchNid,
|
||||||
|
out ExportedFunction? export)
|
||||||
|
{
|
||||||
|
export = null;
|
||||||
|
dispatchNid = string.Empty;
|
||||||
|
|
||||||
|
if (IsKernelDynlibDlsymIdentifier(symbolName))
|
||||||
|
{
|
||||||
|
dispatchNid = KernelDynlibDlsymAerolibNid;
|
||||||
|
_ = _moduleManager.TryGetExport(dispatchNid, out export);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasAerolibSymbol)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (HleDataSymbols.TryGetAddress(hleSymbol.Nid, out _))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_moduleManager.TryGetExport(hleSymbol.Nid, out export))
|
||||||
|
{
|
||||||
|
dispatchNid = hleSymbol.Nid;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetOrCreateLazyImportStub(
|
||||||
|
string dispatchNid,
|
||||||
|
string symbolName,
|
||||||
|
SysAbiSymbol? hleSymbol,
|
||||||
|
ExportedFunction? export,
|
||||||
|
out ulong guestAddress)
|
||||||
|
{
|
||||||
|
guestAddress = 0;
|
||||||
|
if (string.IsNullOrWhiteSpace(dispatchNid))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_lazyDlsymStubGate)
|
||||||
|
{
|
||||||
|
if (TryGetCachedLazyDlsymAddress(dispatchNid, symbolName, out guestAddress))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!EnsureLazyImportStubPoolMapped())
|
||||||
|
{
|
||||||
|
LogLazyImportStubMaterializationFailure(
|
||||||
|
"import stub region unresolved",
|
||||||
|
dispatchNid,
|
||||||
|
symbolName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryAllocateLazyImportStubSlot(out guestAddress))
|
||||||
|
{
|
||||||
|
LogLazyImportStubMaterializationFailure(
|
||||||
|
"lazy import stub pool exhausted",
|
||||||
|
dispatchNid,
|
||||||
|
symbolName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var previousEntries = _importEntries;
|
||||||
|
var importIndex = previousEntries.Length;
|
||||||
|
var newEntries = new ImportStubEntry[importIndex + 1];
|
||||||
|
if (importIndex > 0)
|
||||||
|
{
|
||||||
|
Array.Copy(previousEntries, newEntries, importIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
newEntries[importIndex] = new ImportStubEntry(guestAddress, dispatchNid, export);
|
||||||
|
|
||||||
|
var hostTrampoline = CreateImportHandlerTrampoline(importIndex);
|
||||||
|
if (hostTrampoline == 0 ||
|
||||||
|
!PatchImportStub((nint)(long)guestAddress, hostTrampoline))
|
||||||
|
{
|
||||||
|
guestAddress = 0;
|
||||||
|
LogLazyImportStubMaterializationFailure(
|
||||||
|
"failed to patch lazy import stub trampoline",
|
||||||
|
dispatchNid,
|
||||||
|
symbolName);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_importEntries = newEntries;
|
||||||
|
|
||||||
|
RegisterDlsymRuntimeSymbolKeys(symbolName, dispatchNid, hleSymbol, guestAddress);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryGetCachedLazyDlsymAddress(string dispatchNid, string symbolName, out ulong guestAddress)
|
||||||
|
{
|
||||||
|
if (_lazyDlsymStubCache.TryGetValue(dispatchNid, out guestAddress) ||
|
||||||
|
_lazyDlsymStubCache.TryGetValue(symbolName, out guestAddress))
|
||||||
|
{
|
||||||
|
return guestAddress >= 0x10000;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterDlsymRuntimeSymbolKeys(
|
||||||
|
string symbolName,
|
||||||
|
string dispatchNid,
|
||||||
|
SysAbiSymbol? hleSymbol,
|
||||||
|
ulong guestAddress)
|
||||||
|
{
|
||||||
|
RegisterDlsymRuntimeSymbolKey(symbolName, guestAddress);
|
||||||
|
RegisterDlsymRuntimeSymbolKey(dispatchNid, guestAddress);
|
||||||
|
|
||||||
|
if (IsKernelDynlibDlsymIdentifier(symbolName) || IsKernelDynlibDlsymIdentifier(dispatchNid))
|
||||||
|
{
|
||||||
|
RegisterDlsymRuntimeSymbolKey(RuntimeStubNids.KernelDynlibDlsym, guestAddress);
|
||||||
|
RegisterDlsymRuntimeSymbolKey(KernelDynlibDlsymAerolibNid, guestAddress);
|
||||||
|
RegisterDlsymRuntimeSymbolKey("sceKernelDlsym", guestAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (hleSymbol.HasValue)
|
||||||
|
{
|
||||||
|
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.Nid, guestAddress);
|
||||||
|
RegisterDlsymRuntimeSymbolKey(hleSymbol.Value.ExportName, guestAddress);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void RegisterDlsymRuntimeSymbolKey(string key, ulong guestAddress)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrWhiteSpace(key) || !IsRuntimeSymbolAddressUsable(guestAddress))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_runtimeSymbolsByName[key] = guestAddress;
|
||||||
|
_lazyDlsymStubCache[key] = guestAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
private void LogLazyImportStubMaterializationFailure(string reason, string dispatchNid, string symbolName)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] Lazy import stub materialization failed ({reason}): " +
|
||||||
|
$"nid={dispatchNid} symbol='{symbolName}'");
|
||||||
|
}
|
||||||
|
|
||||||
|
private unsafe bool TryResolveImportStubRegionBounds(
|
||||||
|
ImportStubEntry[] importEntries,
|
||||||
|
out ulong regionBase,
|
||||||
|
out ulong regionLimit)
|
||||||
|
{
|
||||||
|
regionBase = 0;
|
||||||
|
regionLimit = 0;
|
||||||
|
|
||||||
|
for (var candidateIndex = 0; candidateIndex < 64; candidateIndex++)
|
||||||
|
{
|
||||||
|
var candidateBase = ImportStubRegionCanonicalBase -
|
||||||
|
(ulong)candidateIndex * ImportStubRegionAddressStride;
|
||||||
|
if (VirtualQuery((void*)candidateBase, out var memoryInfo, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
|
||||||
|
memoryInfo.RegionSize == 0 ||
|
||||||
|
memoryInfo.State != 4096)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidateLimit = candidateBase + memoryInfo.RegionSize;
|
||||||
|
var hasStub = false;
|
||||||
|
for (var i = 0; i < importEntries.Length; i++)
|
||||||
|
{
|
||||||
|
var entryAddress = importEntries[i].Address;
|
||||||
|
if (entryAddress < candidateBase || entryAddress >= candidateLimit)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((entryAddress - candidateBase) % LazyImportStubSlotSize != 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
hasStub = true;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!hasStub)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
regionBase = candidateBase;
|
||||||
|
regionLimit = candidateLimit;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong maxStubEnd = 0;
|
||||||
|
for (var i = 0; i < importEntries.Length; i++)
|
||||||
|
{
|
||||||
|
var entryAddress = importEntries[i].Address;
|
||||||
|
if (entryAddress < ImportStubRegionCanonicalBase)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if ((entryAddress - ImportStubRegionCanonicalBase) % LazyImportStubSlotSize != 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entryEnd = entryAddress + LazyImportStubSlotSize;
|
||||||
|
if (entryEnd > maxStubEnd)
|
||||||
|
{
|
||||||
|
maxStubEnd = entryEnd;
|
||||||
|
regionBase = ImportStubRegionCanonicalBase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (regionBase == 0 || maxStubEnd <= regionBase)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var spanBytes = maxStubEnd - regionBase;
|
||||||
|
regionLimit = regionBase + AlignUp(spanBytes, ImportStubRegionPageSize);
|
||||||
|
return regionLimit > regionBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool EnsureLazyImportStubPoolMapped()
|
||||||
|
{
|
||||||
|
if (_lazyImportStubPoolMapped && _lazyImportStubPoolBase != 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
var importEntries = _importEntries;
|
||||||
|
if (!TryResolveImportStubRegionBounds(importEntries, out var importStubRegionBase, out var importStubRegionLimit))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong nextSlot = importStubRegionBase;
|
||||||
|
for (var i = 0; i < importEntries.Length; i++)
|
||||||
|
{
|
||||||
|
var entryAddress = importEntries[i].Address;
|
||||||
|
if (entryAddress < importStubRegionBase || entryAddress >= importStubRegionLimit)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var entryEnd = entryAddress + LazyImportStubSlotSize;
|
||||||
|
if (entryEnd > nextSlot)
|
||||||
|
{
|
||||||
|
nextSlot = entryEnd;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (nextSlot >= importStubRegionLimit)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
_lazyImportStubPoolBase = importStubRegionBase;
|
||||||
|
_lazyImportStubNextSlot = nextSlot;
|
||||||
|
_lazyImportStubPoolLimit = importStubRegionLimit;
|
||||||
|
_lazyImportStubPoolMapped = true;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private bool TryAllocateLazyImportStubSlot(out ulong guestAddress)
|
||||||
|
{
|
||||||
|
guestAddress = 0;
|
||||||
|
if (!_lazyImportStubPoolMapped ||
|
||||||
|
_lazyImportStubNextSlot < _lazyImportStubPoolBase ||
|
||||||
|
_lazyImportStubNextSlot + LazyImportStubSlotSize > _lazyImportStubPoolLimit)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
guestAddress = _lazyImportStubNextSlot;
|
||||||
|
_lazyImportStubNextSlot += LazyImportStubSlotSize;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address)
|
private bool TryResolveRuntimeSymbolAlias(string symbolName, out ulong address)
|
||||||
{
|
{
|
||||||
address = 0;
|
address = 0;
|
||||||
@@ -1312,29 +1859,20 @@ public sealed partial class DirectExecutionBackend
|
|||||||
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong bridgeHandle = cpuContext[CpuRegister.Rdi];
|
|
||||||
ulong symbolNameAddress = cpuContext[CpuRegister.Rsi];
|
|
||||||
ulong outputAddress = cpuContext[CpuRegister.Rdx];
|
ulong outputAddress = cpuContext[CpuRegister.Rdx];
|
||||||
_ = TryReadAsciiZ(symbolNameAddress, 512, out var symbolName);
|
try
|
||||||
|
|
||||||
OrbisGen2Result result = DispatchKernelDynlibDlsym();
|
|
||||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
|
||||||
{
|
{
|
||||||
return result;
|
OrbisGen2Result result = DispatchKernelDynlibDlsym();
|
||||||
|
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (_logBootstrap)
|
catch
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
return CompleteKernelDynlibDlsymFailure(cpuContext, outputAddress);
|
||||||
$"[LOADER][TRACE] bootstrap_dispatch: handle=0x{bridgeHandle:X16} symbol='{symbolName}' out=0x{outputAddress:X16} rax=0x{cpuContext[CpuRegister.Rax]:X16}");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (cpuContext[CpuRegister.Rax] == 0uL)
|
|
||||||
{
|
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
Console.Error.WriteLine(
|
|
||||||
$"[LOADER][WARN] bootstrap_bridge unresolved: handle=0x{bridgeHandle:X} symbol='{symbolName}' out=0x{outputAddress:X16}");
|
|
||||||
return OrbisGen2Result.ORBIS_GEN2_OK;
|
return OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1375,6 +1913,7 @@ public sealed partial class DirectExecutionBackend
|
|||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
|
List<byte> list = new List<byte>(Math.Min(maxLength, 256));
|
||||||
Span<byte> destination = stackalloc byte[1];
|
Span<byte> destination = stackalloc byte[1];
|
||||||
for (int i = 0; i < maxLength; i++)
|
for (int i = 0; i < maxLength; i++)
|
||||||
|
|||||||
@@ -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.Cpu;
|
||||||
using SharpEmu.Core.Memory;
|
using SharpEmu.Core.Memory;
|
||||||
using SharpEmu.HLE;
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Loader;
|
namespace SharpEmu.Core.Loader;
|
||||||
|
|
||||||
public sealed class SelfLoader : ISelfLoader
|
public sealed class SelfLoader : ISelfLoader
|
||||||
{
|
{
|
||||||
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
||||||
private const uint SelfMagic = 0x4F153D1D;
|
private const uint SelfMagic = 0x4F153D1D;
|
||||||
private const ulong SelfSegmentFlag = 0x800;
|
private const ulong SelfSegmentFlag = 0x800;
|
||||||
private const int PageSize = 0x1000;
|
private const int PageSize = 0x1000;
|
||||||
@@ -155,7 +157,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
}
|
}
|
||||||
|
|
||||||
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
|
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
|
||||||
Console.Error.WriteLine($"[LOADER][TLS] load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
|
Log.Debug($"TLS load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
|
||||||
|
|
||||||
var loadContext = ParseLayout(imageData);
|
var loadContext = ParseLayout(imageData);
|
||||||
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
||||||
@@ -255,7 +257,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
_nextTlsModuleId++;
|
_nextTlsModuleId++;
|
||||||
}
|
}
|
||||||
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||||
|
|
||||||
return new SelfImage(
|
return new SelfImage(
|
||||||
loadContext.IsSelf,
|
loadContext.IsSelf,
|
||||||
@@ -403,15 +405,15 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
var virtualAddress = header.VirtualAddress + imageBase;
|
var virtualAddress = header.VirtualAddress + imageBase;
|
||||||
|
|
||||||
Console.Error.WriteLine($"[LOADER] Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
Log.Debug($"Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
||||||
if (header.Alignment > 1)
|
if (header.Alignment > 1)
|
||||||
{
|
{
|
||||||
var vaddrMod = virtualAddress % header.Alignment;
|
var vaddrMod = virtualAddress % header.Alignment;
|
||||||
var offsetMod = header.Offset % header.Alignment;
|
var offsetMod = header.Offset % header.Alignment;
|
||||||
if (vaddrMod != offsetMod)
|
if (vaddrMod != offsetMod)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Warning(
|
||||||
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
|
$"Segment {index} ELF alignment mismatch! " +
|
||||||
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
||||||
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
|
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
|
||||||
}
|
}
|
||||||
@@ -671,13 +673,13 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||||
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -689,8 +691,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
||||||
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -783,15 +785,15 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!IsSupportedRelocationType(relocation.Type))
|
if (!IsSupportedRelocationType(relocation.Type))
|
||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocation.Type}");
|
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -800,7 +802,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
|
Log.Debug("[FOCUS][SKIP] target address not mapped");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -840,7 +842,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -852,10 +854,9 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||||
if (symbolAddress == 0)
|
if (symbolAddress == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Warning(
|
||||||
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(new RelocationDescriptor(
|
||||||
@@ -873,10 +874,9 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||||
if (symbolAddress == 0)
|
if (symbolAddress == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Warning(
|
||||||
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||||
continue;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
descriptors.Add(new RelocationDescriptor(
|
descriptors.Add(new RelocationDescriptor(
|
||||||
@@ -893,7 +893,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
|
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -902,7 +902,7 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
Log.Debug($"[FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
||||||
}
|
}
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -950,8 +950,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1025,8 +1025,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
if (preInitializers.Count != 0 || initializers.Count != 0)
|
if (preInitializers.Count != 0 || initializers.Count != 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine(
|
Log.Info(
|
||||||
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1409,8 +1409,8 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
|
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
|
||||||
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
||||||
Console.Error.WriteLine(
|
Log.Debug(
|
||||||
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||||
if (mapSize > int.MaxValue)
|
if (mapSize > int.MaxValue)
|
||||||
{
|
{
|
||||||
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
|
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
|
||||||
@@ -1842,16 +1842,16 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
|
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
|
||||||
|
|
||||||
var guestAddr = location + imageBase;
|
var guestAddr = location + imageBase;
|
||||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
||||||
if (virtualMemory.TryRead(guestAddr, tableBytes))
|
if (virtualMemory.TryRead(guestAddr, tableBytes))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (virtualMemory.TryRead(location, tableBytes))
|
if (virtualMemory.TryRead(location, tableBytes))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
Log.Debug($"TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1859,11 +1859,11 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
{
|
{
|
||||||
var slice = elfData.Slice((int)location, (int)size);
|
var slice = elfData.Slice((int)location, (int)size);
|
||||||
tableBytes = slice.ToArray();
|
tableBytes = slice.ToArray();
|
||||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
|
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
|
||||||
tableBytes = Array.Empty<byte>();
|
tableBytes = Array.Empty<byte>();
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
@@ -1920,17 +1920,17 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
|
|
||||||
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
|
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TEST] ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
||||||
|
|
||||||
if (address == 0)
|
if (address == 0)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
|
Log.Debug("-> return 0 (null)");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
|
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TEST] -> resolved raw 0x{resolved:X16}");
|
Log.Debug($"-> resolved raw 0x{resolved:X16}");
|
||||||
return resolved;
|
return resolved;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1939,18 +1939,18 @@ public sealed class SelfLoader : ISelfLoader
|
|||||||
var rebased = address + imageBase;
|
var rebased = address + imageBase;
|
||||||
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
|
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TEST] -> resolved rebased 0x{resolvedRebased:X16}");
|
Log.Debug($"-> resolved rebased 0x{resolvedRebased:X16}");
|
||||||
return resolvedRebased;
|
return resolvedRebased;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (address < 0x10000)
|
if (address < 0x10000)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
|
Log.Debug($"-> reject small 0x{address:X}");
|
||||||
return 0;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
|
Log.Debug($"-> fallback raw 0x{address:X}");
|
||||||
return address;
|
return address;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ using System.Buffers.Binary;
|
|||||||
using System.Collections.Generic;
|
using System.Collections.Generic;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
using System.Text;
|
using System.Text;
|
||||||
|
using System.IO;
|
||||||
|
|
||||||
namespace SharpEmu.Core.Runtime;
|
namespace SharpEmu.Core.Runtime;
|
||||||
|
|
||||||
@@ -82,7 +83,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
|
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
|
||||||
};
|
};
|
||||||
var moduleManager = new ModuleManager();
|
var moduleManager = new ModuleManager();
|
||||||
moduleManager.RegisterFromAssembly(typeof(VideoOutExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
|
||||||
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
moduleManager.RegisterFromAssembly(typeof(KernelExports).Assembly, Generation.Gen4 | Generation.Gen5, Aerolib.Instance);
|
||||||
moduleManager.Freeze();
|
moduleManager.Freeze();
|
||||||
|
|
||||||
@@ -139,8 +139,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
LastMilestoneLog = null;
|
LastMilestoneLog = null;
|
||||||
KernelModuleRegistry.Reset();
|
KernelModuleRegistry.Reset();
|
||||||
var image = LoadImage(normalizedEbootPath);
|
var image = LoadImage(normalizedEbootPath);
|
||||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
|
||||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||||
|
LogAppBundleInfo(normalizedEbootPath, image);
|
||||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||||
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
||||||
@@ -357,6 +358,66 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
|||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
|
||||||
|
{
|
||||||
|
var executableName = Path.GetFileName(ebootPath);
|
||||||
|
if (string.IsNullOrWhiteSpace(executableName))
|
||||||
|
{
|
||||||
|
executableName = "eboot.bin";
|
||||||
|
}
|
||||||
|
|
||||||
|
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
|
||||||
|
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
|
||||||
|
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
|
||||||
|
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
|
||||||
|
|
||||||
|
var builder = new StringBuilder();
|
||||||
|
builder.AppendLine("App bundle info:");
|
||||||
|
builder.AppendLine($"- Display name: {displayName}");
|
||||||
|
builder.AppendLine($"- Version: {version}");
|
||||||
|
builder.AppendLine($"- Title ID: {titleId}");
|
||||||
|
builder.AppendLine($"- Content ID: {contentId}");
|
||||||
|
builder.AppendLine($"- Executable: {executableName}");
|
||||||
|
builder.Append("- Platform: PlayStation 5");
|
||||||
|
Log.Info(builder.ToString());
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? ResolveContentId(string? bundleRoot)
|
||||||
|
{
|
||||||
|
if (string.IsNullOrEmpty(bundleRoot))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var candidate in new[]
|
||||||
|
{
|
||||||
|
Path.Combine(bundleRoot, "sce_sys", "param.json"),
|
||||||
|
Path.Combine(bundleRoot, "param.json"),
|
||||||
|
})
|
||||||
|
{
|
||||||
|
if (!File.Exists(candidate))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
|
||||||
|
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
|
||||||
|
{
|
||||||
|
return contentId.GetString();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
||||||
{
|
{
|
||||||
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
||||||
|
|||||||
@@ -65,7 +65,10 @@
|
|||||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
},
|
},
|
||||||
"sharpemu.hle": {
|
"sharpemu.hle": {
|
||||||
"type": "Project"
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.Logging": "[1.0.0, )"
|
||||||
|
}
|
||||||
},
|
},
|
||||||
"sharpemu.libs": {
|
"sharpemu.libs": {
|
||||||
"type": "Project",
|
"type": "Project",
|
||||||
|
|||||||
@@ -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.
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -85,6 +85,14 @@ internal sealed class EmulatorProcess : IDisposable
|
|||||||
return;
|
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)
|
if (_processHandle != 0)
|
||||||
{
|
{
|
||||||
_ = TerminateProcess(_processHandle, 1);
|
_ = TerminateProcess(_processHandle, 1);
|
||||||
@@ -160,8 +168,7 @@ internal sealed class EmulatorProcess : IDisposable
|
|||||||
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||||
var policy2 =
|
var policy2 =
|
||||||
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
|
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_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
|
||||||
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
|
||||||
|
|
||||||
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
||||||
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
||||||
@@ -192,29 +199,10 @@ internal sealed class EmulatorProcess : IDisposable
|
|||||||
ref startupInfoEx,
|
ref startupInfoEx,
|
||||||
out var processInfo);
|
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)
|
if (!created)
|
||||||
{
|
{
|
||||||
var error = Marshal.GetLastWin32Error();
|
var error = Marshal.GetLastWin32Error();
|
||||||
throw new Win32Exception(error, $"Failed to start '{exePath}' (Win32 error {error}: {new Win32Exception(error).Message}).");
|
throw new Win32Exception(error, $"Failed to start '{exePath}' with CET/CFG mitigation disabled (Win32 error {error}: {new Win32Exception(error).Message}).");
|
||||||
}
|
}
|
||||||
|
|
||||||
CloseHandle(processInfo.hThread);
|
CloseHandle(processInfo.hThread);
|
||||||
@@ -639,6 +627,10 @@ internal sealed class EmulatorProcess : IDisposable
|
|||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool TerminateProcess(nint process, uint exitCode);
|
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)]
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool CloseHandle(nint handle);
|
private static extern bool CloseHandle(nint handle);
|
||||||
|
|||||||
@@ -23,8 +23,35 @@ public sealed class GuiSettings
|
|||||||
|
|
||||||
public bool StrictDynlibResolution { 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; }
|
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 emulator is portable and keeps its data next to the executable;
|
||||||
// the GUI follows the same convention.
|
// the GUI follows the same convention.
|
||||||
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "gui-settings.json");
|
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "gui-settings.json");
|
||||||
|
|||||||
@@ -14,7 +14,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
ExtendClientAreaToDecorationsHint="True"
|
ExtendClientAreaToDecorationsHint="True"
|
||||||
ExtendClientAreaChromeHints="PreferSystemChrome"
|
ExtendClientAreaChromeHints="PreferSystemChrome"
|
||||||
ExtendClientAreaTitleBarHeightHint="44"
|
ExtendClientAreaTitleBarHeightHint="44"
|
||||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico">
|
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
|
||||||
|
KeyDown="OnKeyDown">
|
||||||
|
|
||||||
<Grid RowDefinitions="44,*,32">
|
<Grid RowDefinitions="44,*,32">
|
||||||
|
|
||||||
@@ -43,7 +44,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<!-- Title bar -->
|
<!-- Title bar -->
|
||||||
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
<Grid x:Name="TitleBar" Grid.Row="0" Background="{StaticResource ChromeBrush}">
|
||||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||||
<Image Source="avares://SharpEmu.GUI/Assets/logo.png" Width="20" Height="20" />
|
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
||||||
|
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||||
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||||
<Border Classes="pill">
|
<Border Classes="pill">
|
||||||
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||||
@@ -158,13 +160,17 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
|
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
|
||||||
Margin="0,12,0,0" IsVisible="False">
|
Margin="0,12,0,0" IsVisible="False">
|
||||||
<Grid RowDefinitions="Auto,*">
|
<Grid RowDefinitions="Auto,*">
|
||||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto" Margin="16,12,16,8">
|
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
|
||||||
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
<TextBlock Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||||
<CheckBox Grid.Column="1" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
|
||||||
FontSize="12" Margin="0,0,12,0" />
|
Watermark="Search..." Width="320" />
|
||||||
<Button Grid.Column="2" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
<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" />
|
Padding="10,4" Margin="0,0,8,0" />
|
||||||
<Button Grid.Column="3" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
<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" />
|
Padding="10,4" />
|
||||||
</Grid>
|
</Grid>
|
||||||
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
||||||
@@ -243,10 +249,31 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
<NumericUpDown x:Name="TraceImportsBox" Width="170" Minimum="0" Maximum="4096" Increment="16"
|
||||||
Value="0" FormatString="0" CornerRadius="8" />
|
Value="0" FormatString="0" CornerRadius="8" />
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
<StackPanel>
|
<StackPanel Margin="0,0,24,0">
|
||||||
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
<TextBlock Classes="fieldLabel" Text="Strict dynlib resolution" />
|
||||||
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" />
|
||||||
</StackPanel>
|
</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>
|
</WrapPanel>
|
||||||
</StackPanel>
|
</StackPanel>
|
||||||
</Border>
|
</Border>
|
||||||
|
|||||||
@@ -1,21 +1,24 @@
|
|||||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
using System.Collections.Concurrent;
|
|
||||||
using System.Collections.ObjectModel;
|
|
||||||
using System.Reflection;
|
|
||||||
using System.Text.Json;
|
|
||||||
using System.Diagnostics;
|
|
||||||
using Avalonia;
|
using Avalonia;
|
||||||
|
using Avalonia.Collections;
|
||||||
using Avalonia.Controls;
|
using Avalonia.Controls;
|
||||||
using Avalonia.Input;
|
using Avalonia.Input;
|
||||||
using Avalonia.Interactivity;
|
using Avalonia.Interactivity;
|
||||||
using Avalonia.Media;
|
using Avalonia.Media;
|
||||||
using Avalonia.Media.Imaging;
|
using Avalonia.Media.Imaging;
|
||||||
|
using Avalonia.Platform;
|
||||||
using Avalonia.Platform.Storage;
|
using Avalonia.Platform.Storage;
|
||||||
using Avalonia.Threading;
|
using Avalonia.Threading;
|
||||||
using Avalonia.VisualTree;
|
using Avalonia.VisualTree;
|
||||||
using SharpEmu.Libs.Pad;
|
using SharpEmu.Libs.Pad;
|
||||||
|
using SharpEmu.Logging;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Collections.ObjectModel;
|
||||||
|
using System.Diagnostics;
|
||||||
|
using System.Reflection;
|
||||||
|
using System.Text.Json;
|
||||||
|
|
||||||
namespace SharpEmu.GUI;
|
namespace SharpEmu.GUI;
|
||||||
|
|
||||||
@@ -32,15 +35,26 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private readonly List<GameEntry> _allGames = new();
|
private readonly List<GameEntry> _allGames = new();
|
||||||
private readonly ObservableCollection<GameEntry> _visibleGames = new();
|
private readonly ObservableCollection<GameEntry> _visibleGames = new();
|
||||||
private readonly ObservableCollection<LogLine> _consoleLines = new();
|
private readonly AvaloniaList<LogLine> _consoleLines = new();
|
||||||
|
private readonly List<LogLine> _allConsoleLines = new();
|
||||||
private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new();
|
private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new();
|
||||||
private readonly DispatcherTimer _consoleFlushTimer;
|
private readonly DispatcherTimer _consoleFlushTimer;
|
||||||
|
|
||||||
private GuiSettings _settings = new();
|
private GuiSettings _settings = new();
|
||||||
private EmulatorProcess? _emulator;
|
private EmulatorProcess? _emulator;
|
||||||
|
private ConsoleWindow? _consoleWindow;
|
||||||
|
private StreamWriter? _fileLog;
|
||||||
|
private readonly SndPreviewPlayer _sndPreview = new();
|
||||||
private string? _emulatorExePath;
|
private string? _emulatorExePath;
|
||||||
private bool _isRunning;
|
private bool _isRunning;
|
||||||
private int _autoScrollTicks;
|
private int _autoScrollTicks;
|
||||||
|
|
||||||
|
// Discord Rich Presence state.
|
||||||
|
private readonly long _launcherStartUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
private DiscordRichPresence? _discord;
|
||||||
|
private string? _runningGameName;
|
||||||
|
private string? _runningGameTitleId;
|
||||||
|
private long _runningSinceUnixSeconds;
|
||||||
private int _detailLoadGeneration;
|
private int _detailLoadGeneration;
|
||||||
private int _backdropGeneration;
|
private int _backdropGeneration;
|
||||||
|
|
||||||
@@ -74,16 +88,27 @@ public partial class MainWindow : Window
|
|||||||
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
|
||||||
GameList.DoubleTapped += (_, _) => LaunchSelected();
|
GameList.DoubleTapped += (_, _) => LaunchSelected();
|
||||||
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
SearchBox.TextChanged += (_, _) => RefreshVisibleGames();
|
||||||
|
ConsoleSearchBox.TextChanged += (_, _) => RefreshVisibleConsoleLines();
|
||||||
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
AddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||||
EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
EmptyAddFolderButton.Click += async (_, _) => await AddFolderAsync();
|
||||||
RescanButton.Click += async (_, _) => await RescanLibraryAsync();
|
RescanButton.Click += async (_, _) => await RescanLibraryAsync();
|
||||||
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
|
OpenFileButton.Click += async (_, _) => await OpenFileAsync();
|
||||||
LaunchButton.Click += (_, _) => LaunchSelected();
|
LaunchButton.Click += (_, _) => LaunchSelected();
|
||||||
StopButton.Click += (_, _) => _emulator?.Stop();
|
StopButton.Click += (_, _) => _emulator?.Stop();
|
||||||
|
ClearLogButton.Click += (_, _) => { _consoleLines.Clear(); _allConsoleLines.Clear(); };
|
||||||
|
StopButton.Click += (_, _) => StopEmulator();
|
||||||
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
|
ClearLogButton.Click += (_, _) => _consoleLines.Clear();
|
||||||
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
|
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
|
||||||
|
DetachConsoleButton.Click += (_, _) => ShowConsoleWindow();
|
||||||
OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true;
|
OptionsToggle.IsCheckedChanged += (_, _) => OptionsPanel.IsVisible = OptionsToggle.IsChecked == true;
|
||||||
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true;
|
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
|
||||||
|
SelectLogFilePathButton.Click += async (_, _) => await SelectFilePathAsync();
|
||||||
|
TitleMusicToggle.IsCheckedChanged += (_, _) => OnTitleMusicToggled();
|
||||||
|
DiscordToggle.IsCheckedChanged += (_, _) =>
|
||||||
|
{
|
||||||
|
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
||||||
|
UpdateDiscordPresence();
|
||||||
|
};
|
||||||
|
|
||||||
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
GameList.AddHandler(ContextRequestedEvent, OnGameContextRequested, RoutingStrategies.Tunnel);
|
||||||
CtxLaunch.Click += (_, _) => LaunchSelected();
|
CtxLaunch.Click += (_, _) => LaunchSelected();
|
||||||
@@ -98,6 +123,7 @@ public partial class MainWindow : Window
|
|||||||
Closing += (_, _) => OnWindowClosing();
|
Closing += (_, _) => OnWindowClosing();
|
||||||
|
|
||||||
DualSenseReader.EnsureStarted();
|
DualSenseReader.EnsureStarted();
|
||||||
|
XInputReader.EnsureStarted();
|
||||||
_gamepadTimer = new DispatcherTimer
|
_gamepadTimer = new DispatcherTimer
|
||||||
{
|
{
|
||||||
Interval = TimeSpan.FromMilliseconds(50),
|
Interval = TimeSpan.FromMilliseconds(50),
|
||||||
@@ -110,7 +136,8 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void PollGamepad()
|
private void PollGamepad()
|
||||||
{
|
{
|
||||||
if (!DualSenseReader.TryGetState(out var pad))
|
// DualSense wins when both are connected; XInput covers Xbox pads.
|
||||||
|
if (!DualSenseReader.TryGetState(out var pad) && !XInputReader.TryGetState(out pad))
|
||||||
{
|
{
|
||||||
_previousPadButtons = 0;
|
_previousPadButtons = 0;
|
||||||
return;
|
return;
|
||||||
@@ -158,7 +185,7 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
if ((pressed & 0x2000) != 0) // Circle
|
if ((pressed & 0x2000) != 0) // Circle
|
||||||
{
|
{
|
||||||
_emulator?.Stop();
|
StopEmulator();
|
||||||
}
|
}
|
||||||
|
|
||||||
_previousPadButtons = pad.Buttons;
|
_previousPadButtons = pad.Buttons;
|
||||||
@@ -216,24 +243,97 @@ public partial class MainWindow : Window
|
|||||||
private async Task OnOpenedAsync()
|
private async Task OnOpenedAsync()
|
||||||
{
|
{
|
||||||
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
var version = Assembly.GetExecutingAssembly().GetName().Version;
|
||||||
if (version is not null)
|
var display = version is not null ? $"v{version.ToString(3)}" : "v0.0.1";
|
||||||
{
|
display += BuildInfo.CommitSha is null
|
||||||
VersionText.Text = $"v{version.ToString(3)}";
|
? " · dev"
|
||||||
}
|
: BuildInfo.IsOfficialRelease
|
||||||
|
? $" · {BuildInfo.CommitSha}"
|
||||||
|
: $" · UNOFFICIAL {BuildInfo.CommitSha}";
|
||||||
|
VersionText.Text = display;
|
||||||
|
Title = $"SharpEmu {display}";
|
||||||
|
ToolTip.SetTip(VersionText, BuildInfo.Banner);
|
||||||
|
|
||||||
_settings = GuiSettings.Load();
|
_settings = GuiSettings.Load();
|
||||||
ApplySettingsToControls();
|
ApplySettingsToControls();
|
||||||
LocateEmulator();
|
LocateEmulator();
|
||||||
|
UpdateDiscordPresence();
|
||||||
await RescanLibraryAsync();
|
await RescanLibraryAsync();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ---- Discord Rich Presence ----
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Publishes the launcher state to Discord: browsing while idle, the
|
||||||
|
/// running game (with elapsed time) during emulation. No-ops when
|
||||||
|
/// disabled or when no Discord application ID is configured.
|
||||||
|
/// </summary>
|
||||||
|
private void UpdateDiscordPresence()
|
||||||
|
{
|
||||||
|
if (!_settings.DiscordRichPresence || _settings.DiscordClientId.Length == 0)
|
||||||
|
{
|
||||||
|
_discord?.Dispose();
|
||||||
|
_discord = null;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_discord ??= new DiscordRichPresence(_settings.DiscordClientId);
|
||||||
|
if (_isRunning && _runningGameName is { } gameName)
|
||||||
|
{
|
||||||
|
_discord.SetPresence(
|
||||||
|
$"Playing {gameName}",
|
||||||
|
_runningGameTitleId,
|
||||||
|
_runningSinceUnixSeconds);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
// Discord does not render activities without timestamps, so the
|
||||||
|
// browsing state carries the launcher's start time.
|
||||||
|
_discord.SetPresence(
|
||||||
|
"Browsing the library",
|
||||||
|
$"{_allGames.Count} game(s)",
|
||||||
|
_launcherStartUnixSeconds);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnKeyDown(object sender, KeyEventArgs args)
|
||||||
|
{
|
||||||
|
args.Handled = true;
|
||||||
|
switch (args.Key)
|
||||||
|
{
|
||||||
|
case Key.F11:
|
||||||
|
OnWindowFullScreen(this, new RoutedEventArgs());
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
args.Handled = false;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnWindowFullScreen(object sender, RoutedEventArgs args)
|
||||||
|
{
|
||||||
|
if (WindowState == WindowState.FullScreen)
|
||||||
|
{
|
||||||
|
WindowState = WindowState.Normal;
|
||||||
|
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.PreferSystemChrome;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
WindowState = WindowState.FullScreen;
|
||||||
|
ExtendClientAreaChromeHints = ExtendClientAreaChromeHints.NoChrome;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private void OnWindowClosing()
|
private void OnWindowClosing()
|
||||||
{
|
{
|
||||||
ReadControlsIntoSettings();
|
ReadControlsIntoSettings();
|
||||||
_settings.Save();
|
_settings.Save();
|
||||||
_consoleFlushTimer.Stop();
|
_consoleFlushTimer.Stop();
|
||||||
_gamepadTimer.Stop();
|
_gamepadTimer.Stop();
|
||||||
|
_sndPreview.Stop();
|
||||||
|
_discord?.Dispose();
|
||||||
|
_consoleWindow?.Close();
|
||||||
_emulator?.Dispose();
|
_emulator?.Dispose();
|
||||||
|
DropFileLog();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
|
private void OnTitleBarPointerPressed(object? sender, PointerPressedEventArgs e)
|
||||||
@@ -260,6 +360,11 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
|
||||||
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
|
||||||
|
LogToFileToggle.IsChecked = _settings.LogToFile;
|
||||||
|
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
|
||||||
|
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
|
||||||
|
ToolTip.SetTip(SelectLogFilePathButton, string.IsNullOrWhiteSpace(_settings.LogFilePath) ? "No path selected" : _settings.LogFilePath);
|
||||||
|
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void ReadControlsIntoSettings()
|
private void ReadControlsIntoSettings()
|
||||||
@@ -267,6 +372,10 @@ public partial class MainWindow : Window
|
|||||||
_settings.LogLevel = SelectedLogLevel();
|
_settings.LogLevel = SelectedLogLevel();
|
||||||
_settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
_settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
|
||||||
_settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
_settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
|
||||||
|
_settings.LogToFile = LogToFileToggle.IsChecked == true;
|
||||||
|
_settings.OverrideLogFile = OverrideLogFileToggle.IsChecked == true;
|
||||||
|
_settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true;
|
||||||
|
_settings.DiscordRichPresence = DiscordToggle.IsChecked == true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private string SelectedLogLevel()
|
private string SelectedLogLevel()
|
||||||
@@ -365,6 +474,7 @@ public partial class MainWindow : Window
|
|||||||
_allGames.AddRange(games);
|
_allGames.AddRange(games);
|
||||||
RefreshVisibleGames();
|
RefreshVisibleGames();
|
||||||
LoadGameDetailsInBackground(games);
|
LoadGameDetailsInBackground(games);
|
||||||
|
UpdateDiscordPresence();
|
||||||
StatusBarRight.Text = folders.Length == 0
|
StatusBarRight.Text = folders.Length == 0
|
||||||
? "Add a game folder to populate the library."
|
? "Add a game folder to populate the library."
|
||||||
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
|
: $"Library scanned: {games.Count} game(s) in {folders.Length} folder(s).";
|
||||||
@@ -778,6 +888,7 @@ public partial class MainWindow : Window
|
|||||||
SelectedGamePath.Text = game.Path;
|
SelectedGamePath.Text = game.Path;
|
||||||
SelectedCoverPanel.DataContext = game;
|
SelectedCoverPanel.DataContext = game;
|
||||||
_ = UpdateBackdropAsync(game);
|
_ = UpdateBackdropAsync(game);
|
||||||
|
PlaySelectedGamePreview(game);
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -785,11 +896,66 @@ public partial class MainWindow : Window
|
|||||||
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
|
SelectedGamePath.Text = "Pick a game from the library, or open an eboot.bin directly.";
|
||||||
SelectedCoverPanel.DataContext = null;
|
SelectedCoverPanel.DataContext = null;
|
||||||
_ = UpdateBackdropAsync(null);
|
_ = UpdateBackdropAsync(null);
|
||||||
|
_sndPreview.Stop();
|
||||||
}
|
}
|
||||||
|
|
||||||
UpdateRunButtons();
|
UpdateRunButtons();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Loops the selected game's sce_sys/snd0.at9 preview music, console
|
||||||
|
/// home screen style. Silent while a game is running or when disabled
|
||||||
|
/// in the options.
|
||||||
|
/// </summary>
|
||||||
|
private void PlaySelectedGamePreview(GameEntry game)
|
||||||
|
{
|
||||||
|
if (_isRunning || !_settings.PlayTitleMusic)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var directory = Path.GetDirectoryName(game.Path);
|
||||||
|
var sndPath = directory is null ? null : Path.Combine(directory, "sce_sys", "snd0.at9");
|
||||||
|
if (sndPath is not null && File.Exists(sndPath))
|
||||||
|
{
|
||||||
|
_sndPreview.Play(sndPath);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_sndPreview.Stop();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void OnTitleMusicToggled()
|
||||||
|
{
|
||||||
|
_settings.PlayTitleMusic = TitleMusicToggle.IsChecked == true;
|
||||||
|
if (!_settings.PlayTitleMusic)
|
||||||
|
{
|
||||||
|
_sndPreview.Stop();
|
||||||
|
}
|
||||||
|
else if (GameList.SelectedItem is GameEntry game)
|
||||||
|
{
|
||||||
|
PlaySelectedGamePreview(game);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Pauses the preview music while the window is minimized.</summary>
|
||||||
|
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
|
||||||
|
{
|
||||||
|
base.OnPropertyChanged(change);
|
||||||
|
if (change.Property == WindowStateProperty)
|
||||||
|
{
|
||||||
|
if (WindowState == WindowState.Minimized)
|
||||||
|
{
|
||||||
|
_sndPreview.Pause();
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
_sndPreview.Resume();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Fades the window backdrop to the selected game's key art. The image
|
/// Fades the window backdrop to the selected game's key art. The image
|
||||||
/// decodes off the UI thread and is cached on the entry; a newer
|
/// decodes off the UI thread and is cached on the entry; a newer
|
||||||
@@ -855,11 +1021,11 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
if (GameList.SelectedItem is GameEntry game)
|
if (GameList.SelectedItem is GameEntry game)
|
||||||
{
|
{
|
||||||
Launch(game.Path, game.Name);
|
Launch(game.Path, game.Name, game.TitleId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void Launch(string ebootPath, string displayName)
|
private void Launch(string ebootPath, string displayName, string? titleId = null)
|
||||||
{
|
{
|
||||||
if (_isRunning)
|
if (_isRunning)
|
||||||
{
|
{
|
||||||
@@ -876,6 +1042,7 @@ public partial class MainWindow : Window
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_sndPreview.Stop();
|
||||||
ReadControlsIntoSettings();
|
ReadControlsIntoSettings();
|
||||||
_settings.Save();
|
_settings.Save();
|
||||||
|
|
||||||
@@ -894,10 +1061,56 @@ public partial class MainWindow : Window
|
|||||||
arguments.Add($"--trace-imports={_settings.ImportTraceLimit}");
|
arguments.Add($"--trace-imports={_settings.ImportTraceLimit}");
|
||||||
}
|
}
|
||||||
|
|
||||||
arguments.Add(ebootPath);
|
|
||||||
|
|
||||||
_consoleLines.Clear();
|
_consoleLines.Clear();
|
||||||
ConsoleToggle.IsChecked = true;
|
ConsoleToggle.IsChecked = true;
|
||||||
|
|
||||||
|
// Let the CLI mirror stdout/stderr itself; it sees loader/native
|
||||||
|
// diagnostics before the GUI pipe reader can filter or batch them.
|
||||||
|
DropFileLog();
|
||||||
|
if (_settings.LogToFile)
|
||||||
|
{
|
||||||
|
string filePath;
|
||||||
|
if (!string.IsNullOrWhiteSpace(_settings.LogFilePath))
|
||||||
|
{
|
||||||
|
if (_settings.OverrideLogFile)
|
||||||
|
{
|
||||||
|
filePath = _settings.LogFilePath;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
string path = _settings.LogFilePath;
|
||||||
|
string id = string.IsNullOrWhiteSpace(titleId) ? "UNKNOWN" : titleId;
|
||||||
|
foreach (var invalidChar in Path.GetInvalidFileNameChars())
|
||||||
|
{
|
||||||
|
id = id.Replace(invalidChar.ToString(), "");
|
||||||
|
}
|
||||||
|
string identifier = $"{id}-{DateTime.Now:yyyyMMdd-HHmmss}";
|
||||||
|
|
||||||
|
string? dir = Path.GetDirectoryName(path);
|
||||||
|
string? fileName = Path.GetFileNameWithoutExtension(path);
|
||||||
|
string? extension = Path.GetExtension(path);
|
||||||
|
|
||||||
|
string newFileName = $"{fileName}-{identifier}{extension}";
|
||||||
|
filePath = string.IsNullOrEmpty(dir)
|
||||||
|
? newFileName
|
||||||
|
: Path.Combine(dir, newFileName);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
filePath = BuildLogFilePath(titleId) ?? string.Empty;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!string.IsNullOrEmpty(filePath))
|
||||||
|
{
|
||||||
|
arguments.Add("--log-file");
|
||||||
|
arguments.Add(filePath);
|
||||||
|
AppendConsoleLine($"Log file: {filePath}", DimLineBrush);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
arguments.Add(ebootPath);
|
||||||
|
|
||||||
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
|
AppendConsoleLine($"$ SharpEmu {string.Join(' ', arguments)}", DimLineBrush);
|
||||||
|
|
||||||
var emulator = new EmulatorProcess();
|
var emulator = new EmulatorProcess();
|
||||||
@@ -912,15 +1125,75 @@ public partial class MainWindow : Window
|
|||||||
{
|
{
|
||||||
emulator.Dispose();
|
emulator.Dispose();
|
||||||
AppendConsoleLine($"Failed to start the emulator: {ex.Message}", ErrorLineBrush);
|
AppendConsoleLine($"Failed to start the emulator: {ex.Message}", ErrorLineBrush);
|
||||||
|
DropFileLog();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
_emulator = emulator;
|
_emulator = emulator;
|
||||||
_isRunning = true;
|
_isRunning = true;
|
||||||
|
_runningGameName = displayName;
|
||||||
|
_runningGameTitleId = _allGames
|
||||||
|
.FirstOrDefault(game => game.Path.Equals(ebootPath, StringComparison.OrdinalIgnoreCase))?
|
||||||
|
.TitleId;
|
||||||
|
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
StatusDot.Fill = SuccessLineBrush;
|
StatusDot.Fill = SuccessLineBrush;
|
||||||
StatusText.Text = $"Running — {displayName}";
|
StatusText.Text = $"Running — {displayName}";
|
||||||
StatusBarRight.Text = $"Running {displayName}";
|
StatusBarRight.Text = $"Running {displayName}";
|
||||||
UpdateRunButtons();
|
UpdateRunButtons();
|
||||||
|
UpdateDiscordPresence();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Stops the running game and updates status/presence immediately. The
|
||||||
|
/// process-exit path still runs when the corpse is collected, but a game
|
||||||
|
/// wedged in a GPU driver call can keep its process alive for a long
|
||||||
|
/// time after termination — the launcher should not look (or tell
|
||||||
|
/// Discord it is) "playing" during that window.
|
||||||
|
/// </summary>
|
||||||
|
private void StopEmulator()
|
||||||
|
{
|
||||||
|
if (!_isRunning)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_emulator?.Stop();
|
||||||
|
_runningGameName = null;
|
||||||
|
_runningGameTitleId = null;
|
||||||
|
StatusText.Text = "Stopping…";
|
||||||
|
StatusBarRight.Text = "Stopping…";
|
||||||
|
UpdateDiscordPresence();
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Builds "user/logs/<titleId>-<timestamp>.log" next to the emulator
|
||||||
|
/// executable, following the same portable-data convention as savedata.
|
||||||
|
/// </summary>
|
||||||
|
private string? BuildLogFilePath(string? titleId)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var exeDirectory = Path.GetDirectoryName(_emulatorExePath);
|
||||||
|
if (string.IsNullOrEmpty(exeDirectory))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var logsDirectory = Path.Combine(exeDirectory, "user", "logs");
|
||||||
|
Directory.CreateDirectory(logsDirectory);
|
||||||
|
|
||||||
|
var id = string.IsNullOrWhiteSpace(titleId) ? "UNKNOWN" : titleId;
|
||||||
|
foreach (var invalid in Path.GetInvalidFileNameChars())
|
||||||
|
{
|
||||||
|
id = id.Replace(invalid, '_');
|
||||||
|
}
|
||||||
|
|
||||||
|
return Path.Combine(logsDirectory, $"{id}-{DateTime.Now:yyyyMMdd-HHmmss}.log");
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
return null; // unwritable location: launch continues without a log file
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private void OnEmulatorExited(int exitCode)
|
private void OnEmulatorExited(int exitCode)
|
||||||
@@ -941,11 +1214,15 @@ public partial class MainWindow : Window
|
|||||||
};
|
};
|
||||||
var brush = exitCode == 0 ? SuccessLineBrush : ErrorLineBrush;
|
var brush = exitCode == 0 ? SuccessLineBrush : ErrorLineBrush;
|
||||||
AppendConsoleLine($"Process exited with code {exitCode} ({meaning}).", brush);
|
AppendConsoleLine($"Process exited with code {exitCode} ({meaning}).", brush);
|
||||||
|
CloseFileLogSoon();
|
||||||
|
|
||||||
StatusDot.Fill = exitCode == 0 ? (IBrush)SuccessLineBrush : ErrorLineBrush;
|
StatusDot.Fill = exitCode == 0 ? (IBrush)SuccessLineBrush : ErrorLineBrush;
|
||||||
StatusText.Text = $"Exited with code {exitCode} ({meaning})";
|
StatusText.Text = $"Exited with code {exitCode} ({meaning})";
|
||||||
StatusBarRight.Text = "Idle";
|
StatusBarRight.Text = "Idle";
|
||||||
|
_runningGameName = null;
|
||||||
|
_runningGameTitleId = null;
|
||||||
UpdateRunButtons();
|
UpdateRunButtons();
|
||||||
|
UpdateDiscordPresence();
|
||||||
}
|
}
|
||||||
|
|
||||||
private void UpdateRunButtons()
|
private void UpdateRunButtons()
|
||||||
@@ -955,6 +1232,27 @@ public partial class MainWindow : Window
|
|||||||
OpenFileButton.IsEnabled = !_isRunning;
|
OpenFileButton.IsEnabled = !_isRunning;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private async Task SelectFilePathAsync()
|
||||||
|
{
|
||||||
|
SaveFilePickerResult result = await StorageProvider.SaveFilePickerWithResultAsync(new FilePickerSaveOptions
|
||||||
|
{
|
||||||
|
Title = "Select where to save the Log file",
|
||||||
|
SuggestedFileName = "SharpEmuLog",
|
||||||
|
DefaultExtension = "log",
|
||||||
|
FileTypeChoices =
|
||||||
|
[
|
||||||
|
new FilePickerFileType("Plain Text Files") { Patterns = ["*.txt"] },
|
||||||
|
new FilePickerFileType("Log Files") { Patterns = ["*.log"] }
|
||||||
|
]
|
||||||
|
});
|
||||||
|
|
||||||
|
if (result.File is not null)
|
||||||
|
{
|
||||||
|
_settings.LogFilePath = result.File.Path.LocalPath;
|
||||||
|
ToolTip.SetTip(SelectLogFilePathButton, _settings.LogFilePath);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ---- Console ----
|
// ---- Console ----
|
||||||
|
|
||||||
private void FlushPendingConsoleLines()
|
private void FlushPendingConsoleLines()
|
||||||
@@ -967,30 +1265,34 @@ public partial class MainWindow : Window
|
|||||||
var incoming = new List<LogLine>();
|
var incoming = new List<LogLine>();
|
||||||
while (_pendingLines.TryDequeue(out var pending))
|
while (_pendingLines.TryDequeue(out var pending))
|
||||||
{
|
{
|
||||||
|
WriteFileLog(pending.Line);
|
||||||
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
|
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
|
||||||
}
|
}
|
||||||
|
|
||||||
if (incoming.Count >= MaxConsoleLines)
|
FlushFileLog();
|
||||||
|
|
||||||
|
_allConsoleLines.AddRange(incoming);
|
||||||
|
|
||||||
|
string query = ConsoleSearchBox.Text ?? string.Empty;
|
||||||
|
|
||||||
|
IEnumerable<LogLine> linesToAdd = incoming;
|
||||||
|
if (!string.IsNullOrWhiteSpace(query))
|
||||||
{
|
{
|
||||||
// A burst larger than the cap: keep only the newest lines.
|
linesToAdd = incoming.Where(line =>
|
||||||
_consoleLines.Clear();
|
line.Text != null &&
|
||||||
for (var i = incoming.Count - MaxConsoleLines; i < incoming.Count; i++)
|
line.Text.Contains(query, StringComparison.OrdinalIgnoreCase));
|
||||||
{
|
|
||||||
_consoleLines.Add(incoming[i]);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
else
|
_consoleLines.AddRange(linesToAdd);
|
||||||
|
|
||||||
|
var overflow = _consoleLines.Count - MaxConsoleLines;
|
||||||
|
while (_allConsoleLines.Count > MaxConsoleLines)
|
||||||
{
|
{
|
||||||
var overflow = _consoleLines.Count + incoming.Count - MaxConsoleLines;
|
var droppedLine = _allConsoleLines[0];
|
||||||
for (var i = 0; i < overflow; i++)
|
_allConsoleLines.RemoveAt(0);
|
||||||
|
if (_consoleLines.Count > 0 && _consoleLines[0] == droppedLine)
|
||||||
{
|
{
|
||||||
_consoleLines.RemoveAt(0);
|
_consoleLines.RemoveAt(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
foreach (var line in incoming)
|
|
||||||
{
|
|
||||||
_consoleLines.Add(line);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
_autoScrollTicks = 3;
|
_autoScrollTicks = 3;
|
||||||
@@ -998,11 +1300,120 @@ public partial class MainWindow : Window
|
|||||||
|
|
||||||
private void AppendConsoleLine(string text, IBrush brush)
|
private void AppendConsoleLine(string text, IBrush brush)
|
||||||
{
|
{
|
||||||
_consoleLines.Add(new LogLine(text, brush));
|
WriteFileLog(text);
|
||||||
|
FlushFileLog();
|
||||||
|
|
||||||
|
var line = new LogLine(text, brush);
|
||||||
|
_allConsoleLines.Add(line);
|
||||||
|
|
||||||
|
string query = ConsoleSearchBox.Text ?? string.Empty;
|
||||||
|
if (string.IsNullOrWhiteSpace(query) || (text != null && text.Contains(query, StringComparison.OrdinalIgnoreCase)))
|
||||||
|
{
|
||||||
|
_consoleLines.Add(line);
|
||||||
|
}
|
||||||
|
|
||||||
|
while (_allConsoleLines.Count > MaxConsoleLines)
|
||||||
|
{
|
||||||
|
var droppedLine = _allConsoleLines[0];
|
||||||
|
_allConsoleLines.RemoveAt(0);
|
||||||
|
if (_consoleLines.Count > 0 && _consoleLines[0] == droppedLine)
|
||||||
|
{
|
||||||
|
_consoleLines.RemoveAt(0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
_autoScrollTicks = 3;
|
_autoScrollTicks = 3;
|
||||||
MaybeAutoScroll();
|
MaybeAutoScroll();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void RefreshVisibleConsoleLines()
|
||||||
|
{
|
||||||
|
string query = ConsoleSearchBox.Text ?? string.Empty;
|
||||||
|
|
||||||
|
_consoleLines.Clear();
|
||||||
|
|
||||||
|
if (string.IsNullOrWhiteSpace(query))
|
||||||
|
{
|
||||||
|
_consoleLines.AddRange(_allConsoleLines);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
var filtered = _allConsoleLines.Where(line =>
|
||||||
|
line.Text != null &&
|
||||||
|
line.Text.Contains(query, StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
_consoleLines.AddRange(filtered);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// ---- Console-to-file mirroring ----
|
||||||
|
|
||||||
|
private void WriteFileLog(string text)
|
||||||
|
{
|
||||||
|
if (_fileLog is not { } writer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
writer.Write('[');
|
||||||
|
writer.Write(DateTime.Now.ToString("HH:mm:ss.fff"));
|
||||||
|
writer.Write("] ");
|
||||||
|
writer.WriteLine(text);
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
DropFileLog(); // unwritable (disk full, etc.): stop mirroring
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void FlushFileLog()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
_fileLog?.Flush();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
DropFileLog();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void DropFileLog()
|
||||||
|
{
|
||||||
|
var writer = _fileLog;
|
||||||
|
_fileLog = null;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
writer?.Dispose();
|
||||||
|
}
|
||||||
|
catch (Exception)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The pipe reader threads can deliver a final burst after the exit
|
||||||
|
/// event, so the file stays open for one more flush cycle.
|
||||||
|
/// </summary>
|
||||||
|
private void CloseFileLogSoon()
|
||||||
|
{
|
||||||
|
if (_fileLog is not { } writer)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
DispatcherTimer.RunOnce(() =>
|
||||||
|
{
|
||||||
|
if (ReferenceEquals(_fileLog, writer))
|
||||||
|
{
|
||||||
|
FlushPendingConsoleLines();
|
||||||
|
DropFileLog();
|
||||||
|
}
|
||||||
|
}, TimeSpan.FromMilliseconds(400));
|
||||||
|
}
|
||||||
|
|
||||||
private void MaybeAutoScroll()
|
private void MaybeAutoScroll()
|
||||||
{
|
{
|
||||||
// ScrollToEnd is applied over a few flush-timer ticks because the
|
// ScrollToEnd is applied over a few flush-timer ticks because the
|
||||||
@@ -1056,4 +1467,28 @@ public partial class MainWindow : Window
|
|||||||
var text = string.Join(Environment.NewLine, _consoleLines.Select(line => line.Text));
|
var text = string.Join(Environment.NewLine, _consoleLines.Select(line => line.Text));
|
||||||
await Clipboard.SetTextAsync(text);
|
await Clipboard.SetTextAsync(text);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void ShowConsoleWindow()
|
||||||
|
{
|
||||||
|
if (_consoleWindow is { } window)
|
||||||
|
{
|
||||||
|
window.Activate();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
ConsoleSearchBox.Text = string.Empty;
|
||||||
|
ConsoleToggle.IsChecked = false;
|
||||||
|
ConsolePanel.IsVisible = false;
|
||||||
|
_consoleWindow = new ConsoleWindow(
|
||||||
|
_consoleLines,
|
||||||
|
() => { _consoleLines.Clear(); _allConsoleLines.Clear(); },
|
||||||
|
AutoScrollCheck.IsChecked == true);
|
||||||
|
_consoleWindow.Closed += (_, _) =>
|
||||||
|
{
|
||||||
|
_consoleWindow = null;
|
||||||
|
ConsoleToggle.IsChecked = true;
|
||||||
|
ConsolePanel.IsVisible = true;
|
||||||
|
};
|
||||||
|
_consoleWindow.Show(this);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,6 +12,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<Version>0.0.1</Version>
|
<Version>0.0.1</Version>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<!-- Dependency-free; provides the BuildInfo provenance shown in the
|
||||||
|
title bar. -->
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<PackageReference Include="Avalonia" />
|
<PackageReference Include="Avalonia" />
|
||||||
<PackageReference Include="Avalonia.Desktop" />
|
<PackageReference Include="Avalonia.Desktop" />
|
||||||
@@ -21,16 +27,18 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<AvaloniaResource Include="..\..\assets\images\logo.png" Link="Assets/logo.png" />
|
|
||||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
<!-- The DualSense HID reader is shared with the emulator's pad HLE. It is
|
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||||
dependency-free, so it is compiled in directly rather than pulling a
|
with the emulator's pad HLE. They are dependency-free, so they are
|
||||||
reference to all of SharpEmu.Libs into the launcher. -->
|
compiled in directly rather than pulling a reference to all of
|
||||||
|
SharpEmu.Libs into the launcher. -->
|
||||||
<ItemGroup>
|
<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\HidNative.cs" Link="Input/HidNative.cs" />
|
||||||
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
|
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
|
||||||
|
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|
||||||
</Project>
|
</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);
|
||||||
|
}
|
||||||
@@ -180,6 +180,9 @@
|
|||||||
"type": "Transitive",
|
"type": "Transitive",
|
||||||
"resolved": "2.88.9",
|
"resolved": "2.88.9",
|
||||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||||
|
},
|
||||||
|
"sharpemu.logging": {
|
||||||
|
"type": "Project"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,11 +3,13 @@
|
|||||||
|
|
||||||
using System.Buffers.Binary;
|
using System.Buffers.Binary;
|
||||||
using System.Linq;
|
using System.Linq;
|
||||||
|
using SharpEmu.Logging;
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
public sealed class Aerolib : ISymbolCatalog
|
public sealed class Aerolib : ISymbolCatalog
|
||||||
{
|
{
|
||||||
|
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Aerolib");
|
||||||
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
|
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
|
||||||
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
|
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
|
||||||
|
|
||||||
@@ -108,14 +110,14 @@ public sealed class Aerolib : ISymbolCatalog
|
|||||||
|
|
||||||
if (resourceName == null)
|
if (resourceName == null)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
|
Log.Error("Embedded resource 'aerolib.bin' not found");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||||
if (stream == null)
|
if (stream == null)
|
||||||
{
|
{
|
||||||
Console.Error.WriteLine("[AEROLIB] Failed to open embedded resource stream");
|
Log.Error("Failed to open embedded resource stream");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,11 +147,11 @@ public sealed class Aerolib : ISymbolCatalog
|
|||||||
_byExportName[name] = symbol;
|
_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)
|
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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -3,6 +3,7 @@
|
|||||||
|
|
||||||
using System.Collections.Concurrent;
|
using System.Collections.Concurrent;
|
||||||
using System.Reflection;
|
using System.Reflection;
|
||||||
|
using System.Runtime.CompilerServices;
|
||||||
|
|
||||||
namespace SharpEmu.HLE;
|
namespace SharpEmu.HLE;
|
||||||
|
|
||||||
@@ -12,6 +13,7 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ExportedFunction> _exportTable = new(StringComparer.Ordinal);
|
||||||
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
private readonly ConcurrentDictionary<string, ExportedFunction> _exportNameTable = new(StringComparer.Ordinal);
|
||||||
private readonly object _registrationGate = new();
|
private readonly object _registrationGate = new();
|
||||||
|
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
|
||||||
private bool _isFrozen;
|
private bool _isFrozen;
|
||||||
|
|
||||||
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
public int RegisterFromAssembly(Assembly assembly, Generation generation, ISymbolCatalog? symbolCatalog = null)
|
||||||
@@ -25,6 +27,12 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
throw new InvalidOperationException("Module registration is frozen.");
|
throw new InvalidOperationException("Module registration is frozen.");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Deduplicated: one assembly is reached through many types.
|
||||||
|
if (!_scannedAssemblies.Add((assembly, generation)))
|
||||||
|
{
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
|
|
||||||
var registeredCount = 0;
|
var registeredCount = 0;
|
||||||
var instances = new Dictionary<Type, object>();
|
var instances = new Dictionary<Type, object>();
|
||||||
|
|
||||||
@@ -73,8 +81,195 @@ public sealed class ModuleManager : IModuleManager
|
|||||||
{
|
{
|
||||||
_isFrozen = true;
|
_isFrozen = true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
WarmHleTypeInitializers();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A .cctor or first JIT running on a guest thread's hijacked stack fail-fasts the CLR.
|
||||||
|
// Run every HLE type's initializer and JIT its methods here first, on a host thread.
|
||||||
|
private void WarmHleTypeInitializers()
|
||||||
|
{
|
||||||
|
Assembly[] assemblies;
|
||||||
|
lock (_registrationGate)
|
||||||
|
{
|
||||||
|
assemblies = _scannedAssemblies.Select(entry => entry.Assembly).Distinct().ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
assemblies = WithGuestReachableDependencies(assemblies);
|
||||||
|
var bclWarmed = WarmFrameworkTypeInitializers();
|
||||||
|
|
||||||
|
var warmed = 0;
|
||||||
|
var failed = 0;
|
||||||
|
var jitted = 0;
|
||||||
|
var jitFailed = 0;
|
||||||
|
foreach (var assembly in assemblies)
|
||||||
|
{
|
||||||
|
Type[] types;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
types = assembly.GetTypes();
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException ex)
|
||||||
|
{
|
||||||
|
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||||
|
}
|
||||||
|
|
||||||
|
const BindingFlags allMembers = BindingFlags.Public | BindingFlags.NonPublic |
|
||||||
|
BindingFlags.Instance | BindingFlags.Static | BindingFlags.DeclaredOnly;
|
||||||
|
|
||||||
|
foreach (var type in types)
|
||||||
|
{
|
||||||
|
if (type is null || type.ContainsGenericParameters)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||||
|
warmed++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
// A throw here beats a guest-thread fail-fast later; swallow and continue.
|
||||||
|
failed++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Force-JIT (not execute) every method so no guest thread compiles one first.
|
||||||
|
MethodBase[] members;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
members = type.GetConstructors(allMembers)
|
||||||
|
.Concat<MethodBase>(type.GetMethods(allMembers))
|
||||||
|
.ToArray();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var member in members)
|
||||||
|
{
|
||||||
|
if (member.ContainsGenericParameters || member.IsAbstract || member.MethodImplementationFlags.HasFlag(MethodImplAttributes.InternalCall))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RuntimeHelpers.PrepareMethod(member.MethodHandle);
|
||||||
|
jitted++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
jitFailed++;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[HLE] Warmed {warmed} type initializers ({failed} threw) + JIT-compiled {jitted} methods ({jitFailed} skipped) across {assemblies.Length} HLE assemblies, plus {bclWarmed} framework type initializers.");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Framework .cctors too (but not JIT — the BCL is too large).
|
||||||
|
private static int WarmFrameworkTypeInitializers()
|
||||||
|
{
|
||||||
|
var warmed = 0;
|
||||||
|
foreach (var assembly in AppDomain.CurrentDomain.GetAssemblies())
|
||||||
|
{
|
||||||
|
var name = assembly.GetName().Name;
|
||||||
|
if (name is null || !IsFrameworkAssembly(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
Type[] types;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
types = assembly.GetTypes();
|
||||||
|
}
|
||||||
|
catch (ReflectionTypeLoadException ex)
|
||||||
|
{
|
||||||
|
types = ex.Types.Where(t => t is not null).ToArray()!;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var type in types)
|
||||||
|
{
|
||||||
|
if (type is null || type.ContainsGenericParameters)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
RuntimeHelpers.RunClassConstructor(type.TypeHandle);
|
||||||
|
warmed++;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return warmed;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsFrameworkAssembly(string assemblyName) =>
|
||||||
|
assemblyName.StartsWith("System", StringComparison.Ordinal) ||
|
||||||
|
string.Equals(assemblyName, "netstandard", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// Warm the interop assemblies guest threads reach (e.g. Silk.NET via the flip path).
|
||||||
|
private static Assembly[] WithGuestReachableDependencies(Assembly[] scanned)
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, Assembly>(StringComparer.Ordinal);
|
||||||
|
foreach (var assembly in scanned)
|
||||||
|
{
|
||||||
|
result[assembly.FullName ?? assembly.GetName().Name ?? string.Empty] = assembly;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var assembly in scanned)
|
||||||
|
{
|
||||||
|
AssemblyName[] references;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
references = assembly.GetReferencedAssemblies();
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var reference in references)
|
||||||
|
{
|
||||||
|
var name = reference.Name;
|
||||||
|
if (name is null || !IsGuestReachableInterop(name))
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var loaded = Assembly.Load(reference);
|
||||||
|
result[loaded.FullName ?? name] = loaded;
|
||||||
|
}
|
||||||
|
catch
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result.Values.ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsGuestReachableInterop(string assemblyName) =>
|
||||||
|
assemblyName.StartsWith("Silk.NET", StringComparison.Ordinal) ||
|
||||||
|
assemblyName.StartsWith("SharpEmu", StringComparison.Ordinal);
|
||||||
|
|
||||||
public bool TryGetFunction(string nid, out Delegate function)
|
public bool TryGetFunction(string nid, out Delegate function)
|
||||||
{
|
{
|
||||||
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||||
|
|||||||
@@ -40,6 +40,13 @@ public enum OrbisGen2Result : int
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
ORBIS_GEN2_ERROR_DEADLOCK = unchecked((int)0x8002000B),
|
ORBIS_GEN2_ERROR_DEADLOCK = unchecked((int)0x8002000B),
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that the waited-on object was deleted while the caller was
|
||||||
|
/// blocked on it. Matches the SCE kernel EACCES code that waiters of a
|
||||||
|
/// deleted semaphore observe.
|
||||||
|
/// </summary>
|
||||||
|
ORBIS_GEN2_ERROR_DELETED = unchecked((int)0x8002000D),
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates that the target resource is busy.
|
/// Indicates that the target resource is busy.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
@@ -50,6 +57,13 @@ public enum OrbisGen2Result : int
|
|||||||
/// </summary>
|
/// </summary>
|
||||||
ORBIS_GEN2_ERROR_TRY_AGAIN = unchecked((int)0x80020023),
|
ORBIS_GEN2_ERROR_TRY_AGAIN = unchecked((int)0x80020023),
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Indicates that a blocked wait was canceled (e.g. sceKernelCancelSema).
|
||||||
|
/// Matches the SCE kernel ECANCELED code that canceled semaphore waiters
|
||||||
|
/// observe.
|
||||||
|
/// </summary>
|
||||||
|
ORBIS_GEN2_ERROR_CANCELED = unchecked((int)0x80020055),
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Indicates that behavior is recognized but not implemented yet.
|
/// Indicates that behavior is recognized but not implemented yet.
|
||||||
/// </summary>
|
/// </summary>
|
||||||
|
|||||||
@@ -12,6 +12,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
|||||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||||
</PropertyGroup>
|
</PropertyGroup>
|
||||||
|
|
||||||
|
<ItemGroup>
|
||||||
|
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||||
|
</ItemGroup>
|
||||||
|
|
||||||
<ItemGroup>
|
<ItemGroup>
|
||||||
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||||
</ItemGroup>
|
</ItemGroup>
|
||||||
|
|||||||
@@ -1,6 +1,10 @@
|
|||||||
{
|
{
|
||||||
"version": 2,
|
"version": 2,
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"net10.0": {}
|
"net10.0": {
|
||||||
|
"sharpemu.logging": {
|
||||||
|
"type": "Project"
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
+329
-107
@@ -179,7 +179,6 @@ public static class AgcExports
|
|||||||
private static long _createShaderTraceCount;
|
private static long _createShaderTraceCount;
|
||||||
private static long _packetPayloadTraceCount;
|
private static long _packetPayloadTraceCount;
|
||||||
private static bool _tracedMissingPixelShaderBindings;
|
private static bool _tracedMissingPixelShaderBindings;
|
||||||
private static long _unsatisfiedWaitTraceCount;
|
|
||||||
private static long _shaderTranslationMissTraceCount;
|
private static long _shaderTranslationMissTraceCount;
|
||||||
private static long _translatedDrawTraceCount;
|
private static long _translatedDrawTraceCount;
|
||||||
private static long _standardDmaTraceCount;
|
private static long _standardDmaTraceCount;
|
||||||
@@ -2001,6 +2000,57 @@ public static class AgcExports
|
|||||||
lock (gpuState.Gate)
|
lock (gpuState.Gate)
|
||||||
{
|
{
|
||||||
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||||
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// ABI (reversed from Quake): rdi = array of DCB base addresses (u64 each),
|
||||||
|
// rsi = array of DCB sizes in dwords (u32 each), rdx = buffer count.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "6UzEidRZwkg",
|
||||||
|
ExportName = "sceAgcDriverSubmitMultiDcbs",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceAgcDriver")]
|
||||||
|
public static int DriverSubmitMultiDcbs(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var addressArray = ctx[CpuRegister.Rdi];
|
||||||
|
var sizeArray = ctx[CpuRegister.Rsi];
|
||||||
|
var bufferCount = (uint)ctx[CpuRegister.Rdx];
|
||||||
|
if (addressArray == 0 || sizeArray == 0 || bufferCount == 0 || bufferCount > 4096)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tracePackets = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AGC"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
|
||||||
|
lock (gpuState.Gate)
|
||||||
|
{
|
||||||
|
for (uint i = 0; i < bufferCount; i++)
|
||||||
|
{
|
||||||
|
if (!ctx.TryReadUInt64(addressArray + i * 8, out var commandAddress) ||
|
||||||
|
commandAddress == 0 ||
|
||||||
|
!ctx.TryReadUInt32(sizeArray + i * 4, out var dwordCount) ||
|
||||||
|
dwordCount == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracePackets)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.driver_submit_multi_dcbs index={i}/{bufferCount} " +
|
||||||
|
$"addr=0x{commandAddress:X16} dwords={dwordCount}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ParseSubmittedDcb(ctx, gpuState, gpuState.Graphics, commandAddress, dwordCount, tracePackets);
|
||||||
|
}
|
||||||
|
|
||||||
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2049,6 +2099,7 @@ public static class AgcExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
ParseSubmittedDcb(ctx, gpuState, queueState, commandAddress, dwordCount, tracePackets);
|
||||||
|
DrainResumableDcbs(ctx, gpuState, tracePackets);
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -2170,6 +2221,63 @@ public static class AgcExports
|
|||||||
return ReturnPointer(ctx, commandAddress);
|
return ReturnPointer(ctx, commandAddress);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// WAIT_REG_MEM packets whose condition is not met suspend their DCB into
|
||||||
|
// GpuWaitRegistry. Each submit re-checks every suspended DCB against current guest
|
||||||
|
// memory (labels are advanced by ReleaseMem/WriteData/DmaData packets or by direct
|
||||||
|
// CPU writes) and resumes the ones whose condition is now satisfied. A resumed DCB
|
||||||
|
// can itself write labels that unblock others, so loop until a fixed point.
|
||||||
|
private static void DrainResumableDcbs(
|
||||||
|
CpuContext ctx,
|
||||||
|
SubmittedGpuState gpuState,
|
||||||
|
bool tracePackets)
|
||||||
|
{
|
||||||
|
for (var pass = 0; pass < 256; pass++)
|
||||||
|
{
|
||||||
|
var woken = GpuWaitRegistry.CollectSatisfied((address, is64Bit) =>
|
||||||
|
{
|
||||||
|
if (is64Bit)
|
||||||
|
{
|
||||||
|
return ctx.TryReadUInt64(address, out var value64)
|
||||||
|
? value64
|
||||||
|
: (ulong?)null;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.TryReadUInt32(address, out var value32)
|
||||||
|
? value32
|
||||||
|
: (ulong?)null;
|
||||||
|
});
|
||||||
|
if (woken is null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var waiter in woken)
|
||||||
|
{
|
||||||
|
var remainingDwords = waiter.TotalDwords - waiter.ResumeOffset;
|
||||||
|
if (remainingDwords == 0)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tracePackets)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.resumed addr=0x{waiter.WaitAddress:X16} " +
|
||||||
|
$"resume=0x{waiter.ResumeAddress:X16} dwords={remainingDwords}");
|
||||||
|
}
|
||||||
|
|
||||||
|
var state = waiter.State as SubmittedDcbState ?? gpuState.Graphics;
|
||||||
|
ParseSubmittedDcb(
|
||||||
|
ctx,
|
||||||
|
gpuState,
|
||||||
|
state,
|
||||||
|
waiter.ResumeAddress,
|
||||||
|
remainingDwords,
|
||||||
|
tracePackets);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void ParseSubmittedDcb(
|
private static void ParseSubmittedDcb(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
SubmittedGpuState gpuState,
|
SubmittedGpuState gpuState,
|
||||||
@@ -2201,7 +2309,6 @@ public static class AgcExports
|
|||||||
$"agc.dcb.packet dw={offset} addr=0x{currentAddress:X16} " +
|
$"agc.dcb.packet dw={offset} addr=0x{currentAddress:X16} " +
|
||||||
$"header=0x{header:X8} len=1 type=2");
|
$"header=0x{header:X8} len=1 type=2");
|
||||||
}
|
}
|
||||||
|
|
||||||
offset++;
|
offset++;
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
@@ -2311,16 +2418,91 @@ public static class AgcExports
|
|||||||
state.InstanceCount = Math.Max(instanceCount, 1);
|
state.InstanceCount = Math.Max(instanceCount, 1);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (op == ItNop &&
|
// WAIT_REG_MEM (AGC NOP-encapsulated 32/64-bit variants): when the condition is
|
||||||
register is RWaitMem32 or RWaitMem64 &&
|
// not yet satisfied, suspend this DCB; DrainResumableDcbs resumes it once the
|
||||||
|
// watched memory advances.
|
||||||
|
if (op == ItNop && register is RWaitMem32 or RWaitMem64 &&
|
||||||
length >= (register == RWaitMem32 ? 6u : 9u))
|
length >= (register == RWaitMem32 ? 6u : 9u))
|
||||||
{
|
{
|
||||||
ObserveSubmittedWaitRegMem(ctx, currentAddress, register == RWaitMem64, tracePackets);
|
if (TryParseWaitRegMem(ctx, currentAddress, register == RWaitMem64,
|
||||||
|
out var waitAddr, out var refVal, out var waitMask, out var cmpFunc))
|
||||||
|
{
|
||||||
|
ulong curVal = 0;
|
||||||
|
bool hasCurVal;
|
||||||
|
if (register == RWaitMem64)
|
||||||
|
{
|
||||||
|
hasCurVal = ctx.TryReadUInt64(waitAddr, out curVal);
|
||||||
|
}
|
||||||
|
else if (ctx.TryReadUInt32(waitAddr, out var curVal32))
|
||||||
|
{
|
||||||
|
curVal = curVal32;
|
||||||
|
hasCurVal = true;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
hasCurVal = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var waiter = new GpuWaitRegistry.WaitingDcb
|
||||||
|
{
|
||||||
|
CommandBufferAddress = commandAddress,
|
||||||
|
ResumeAddress = currentAddress + ((ulong)length * sizeof(uint)),
|
||||||
|
TotalDwords = dwordCount,
|
||||||
|
ResumeOffset = offset + length,
|
||||||
|
ReferenceValue = refVal,
|
||||||
|
Mask = waitMask,
|
||||||
|
CompareFunction = cmpFunc,
|
||||||
|
Is64Bit = register == RWaitMem64,
|
||||||
|
State = state,
|
||||||
|
};
|
||||||
|
if (hasCurVal && !GpuWaitRegistry.Compare(waiter, curVal))
|
||||||
|
{
|
||||||
|
GpuWaitRegistry.Register(waitAddr, waiter);
|
||||||
|
|
||||||
|
if (tracePackets)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.suspended addr=0x{waitAddr:X16} ref=0x{refVal:X16} " +
|
||||||
|
$"mask=0x{waitMask:X16} cur=0x{curVal:X16} cmp={cmpFunc}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return; // suspend parsing of this DCB
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (op == ItWaitRegMem && length >= 7)
|
if (op == ItWaitRegMem && length >= 7)
|
||||||
{
|
{
|
||||||
ObserveSubmittedStandardWaitRegMem(ctx, currentAddress, tracePackets);
|
if (TryParseStandardWaitRegMem(ctx, currentAddress,
|
||||||
|
out var waitAddr, out var refVal, out var waitMask, out var cmpFunc) &&
|
||||||
|
ctx.TryReadUInt32(waitAddr, out var curVal))
|
||||||
|
{
|
||||||
|
var waiter = new GpuWaitRegistry.WaitingDcb
|
||||||
|
{
|
||||||
|
CommandBufferAddress = commandAddress,
|
||||||
|
ResumeAddress = currentAddress + ((ulong)length * sizeof(uint)),
|
||||||
|
TotalDwords = dwordCount,
|
||||||
|
ResumeOffset = offset + length,
|
||||||
|
ReferenceValue = refVal,
|
||||||
|
Mask = waitMask,
|
||||||
|
CompareFunction = cmpFunc,
|
||||||
|
Is64Bit = false,
|
||||||
|
State = state,
|
||||||
|
};
|
||||||
|
if (!GpuWaitRegistry.Compare(waiter, curVal))
|
||||||
|
{
|
||||||
|
GpuWaitRegistry.Register(waitAddr, waiter);
|
||||||
|
|
||||||
|
if (tracePackets)
|
||||||
|
{
|
||||||
|
TraceAgc(
|
||||||
|
$"agc.dcb.suspended_std addr=0x{waitAddr:X16} ref=0x{refVal:X16} " +
|
||||||
|
$"mask=0x{waitMask:X16} cur=0x{curVal:X16} cmp={cmpFunc}");
|
||||||
|
}
|
||||||
|
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (TryReadSubmittedDrawCount(
|
if (TryReadSubmittedDrawCount(
|
||||||
@@ -2641,104 +2823,6 @@ public static class AgcExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void ObserveSubmittedWaitRegMem(
|
|
||||||
CpuContext ctx,
|
|
||||||
ulong packetAddress,
|
|
||||||
bool is64Bit,
|
|
||||||
bool tracePacket)
|
|
||||||
{
|
|
||||||
if (!ctx.TryReadUInt64(packetAddress + 4, out var address) ||
|
|
||||||
!ctx.TryReadUInt32(packetAddress + (is64Bit ? 28u : 16u), out var control))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
ulong mask;
|
|
||||||
ulong reference;
|
|
||||||
ulong value;
|
|
||||||
if (is64Bit)
|
|
||||||
{
|
|
||||||
if (!ctx.TryReadUInt64(packetAddress + 12, out mask) ||
|
|
||||||
!ctx.TryReadUInt64(packetAddress + 20, out reference) ||
|
|
||||||
!ctx.TryReadUInt64(address, out value))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
if (!ctx.TryReadUInt32(packetAddress + 12, out var mask32) ||
|
|
||||||
!ctx.TryReadUInt32(packetAddress + 20, out var reference32) ||
|
|
||||||
!ctx.TryReadUInt32(address, out var value32))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
mask = mask32;
|
|
||||||
reference = reference32;
|
|
||||||
value = value32;
|
|
||||||
}
|
|
||||||
|
|
||||||
var compareFunction = control & 0xFFu;
|
|
||||||
TraceSubmittedWait(
|
|
||||||
address,
|
|
||||||
value,
|
|
||||||
mask,
|
|
||||||
reference,
|
|
||||||
compareFunction,
|
|
||||||
is64Bit ? 64 : 32,
|
|
||||||
tracePacket);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ObserveSubmittedStandardWaitRegMem(
|
|
||||||
CpuContext ctx,
|
|
||||||
ulong packetAddress,
|
|
||||||
bool tracePacket)
|
|
||||||
{
|
|
||||||
if (!ctx.TryReadUInt32(packetAddress + 4, out var control) ||
|
|
||||||
!ctx.TryReadUInt64(packetAddress + 8, out var address) ||
|
|
||||||
!ctx.TryReadUInt32(packetAddress + 16, out var reference) ||
|
|
||||||
!ctx.TryReadUInt32(packetAddress + 20, out var mask) ||
|
|
||||||
!ctx.TryReadUInt32(address, out var value))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceSubmittedWait(address, value, mask, reference, control & 0x7u, 32, tracePacket);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void TraceSubmittedWait(
|
|
||||||
ulong address,
|
|
||||||
ulong value,
|
|
||||||
ulong mask,
|
|
||||||
ulong reference,
|
|
||||||
uint compareFunction,
|
|
||||||
int bits,
|
|
||||||
bool tracePacket)
|
|
||||||
{
|
|
||||||
var maskedValue = value & mask;
|
|
||||||
var satisfied = compareFunction switch
|
|
||||||
{
|
|
||||||
0 => false,
|
|
||||||
1 => maskedValue < reference,
|
|
||||||
2 => maskedValue <= reference,
|
|
||||||
3 => maskedValue == reference,
|
|
||||||
4 => maskedValue != reference,
|
|
||||||
5 => maskedValue >= reference,
|
|
||||||
6 => maskedValue > reference,
|
|
||||||
_ => true,
|
|
||||||
};
|
|
||||||
if (!tracePacket && (satisfied || !ShouldTraceHotPath(ref _unsatisfiedWaitTraceCount)))
|
|
||||||
{
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
TraceAgc(
|
|
||||||
$"agc.dcb.wait_reg_mem bits={bits} addr=0x{address:X16} " +
|
|
||||||
$"value=0x{value:X16} mask=0x{mask:X16} ref=0x{reference:X16} " +
|
|
||||||
$"compare={compareFunction} satisfied={satisfied}");
|
|
||||||
}
|
|
||||||
|
|
||||||
private static void ApplySubmittedReleaseMem(
|
private static void ApplySubmittedReleaseMem(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
ulong packetAddress,
|
ulong packetAddress,
|
||||||
@@ -2756,10 +2840,11 @@ public static class AgcExports
|
|||||||
var dataSelection = (control >> 16) & 0xFFu;
|
var dataSelection = (control >> 16) & 0xFFu;
|
||||||
var destinationAddress = ((ulong)destinationHi << 32) | destinationLo;
|
var destinationAddress = ((ulong)destinationHi << 32) | destinationLo;
|
||||||
var data = ((ulong)dataHi << 32) | dataLo;
|
var data = ((ulong)dataHi << 32) | dataLo;
|
||||||
|
|
||||||
var wroteData = dataSelection switch
|
var wroteData = dataSelection switch
|
||||||
{
|
{
|
||||||
1 or 2 => ctx.TryWriteUInt32(destinationAddress, dataLo),
|
1 => ctx.TryWriteUInt32(destinationAddress, dataLo),
|
||||||
3 => ctx.TryWriteUInt64(destinationAddress, data),
|
2 or 3 => ctx.TryWriteUInt64(destinationAddress, data),
|
||||||
_ => false,
|
_ => false,
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -2855,6 +2940,11 @@ public static class AgcExports
|
|||||||
case ItDrawIndex2 when packetLength >= 6:
|
case ItDrawIndex2 when packetLength >= 6:
|
||||||
state.DrawIndexOffset = 0;
|
state.DrawIndexOffset = 0;
|
||||||
return ctx.TryReadUInt32(packetAddress + 16, out drawCount);
|
return ctx.TryReadUInt32(packetAddress + 16, out drawCount);
|
||||||
|
// 5-dword form emitted by DcbDrawIndex (count at +4, ItIndexBase/
|
||||||
|
// ItIndexBufferSize carried by separate preceding packets).
|
||||||
|
case ItDrawIndex2 when packetLength >= 5:
|
||||||
|
state.DrawIndexOffset = 0;
|
||||||
|
return ctx.TryReadUInt32(packetAddress + 4, out drawCount);
|
||||||
case ItDrawIndexOffset2 when packetLength >= 5:
|
case ItDrawIndexOffset2 when packetLength >= 5:
|
||||||
if (!ctx.TryReadUInt32(packetAddress + 8, out var indexOffset))
|
if (!ctx.TryReadUInt32(packetAddress + 8, out var indexOffset))
|
||||||
{
|
{
|
||||||
@@ -3206,10 +3296,60 @@ public static class AgcExports
|
|||||||
globalMemoryBindings,
|
globalMemoryBindings,
|
||||||
vertexInputs,
|
vertexInputs,
|
||||||
renderTargets,
|
renderTargets,
|
||||||
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()));
|
ApplyTransparentPremultipliedFillClear(
|
||||||
|
CreateRenderState(state.CxRegisters, renderTargets.FirstOrDefault()),
|
||||||
|
textures,
|
||||||
|
vertexInputs,
|
||||||
|
pixelEvaluation.InitialScalarRegisters));
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static readonly bool _transparentFillClearEnabled = !string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_TRANSPARENT_FILL_CLEAR"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Chowdren resets its effect layers with an untextured transparent-black
|
||||||
|
/// fill using premultiplied blending. With One/OneMinusSrcAlpha that draw
|
||||||
|
/// is otherwise a no-op, causing fog and vignette layers to accumulate.
|
||||||
|
/// Treat precisely that draw shape as an overwrite.
|
||||||
|
/// </summary>
|
||||||
|
private static VulkanGuestRenderState ApplyTransparentPremultipliedFillClear(
|
||||||
|
VulkanGuestRenderState renderState,
|
||||||
|
IReadOnlyList<TranslatedImageBinding> textures,
|
||||||
|
IReadOnlyList<Gen5VertexInputBinding> vertexInputs,
|
||||||
|
IReadOnlyList<uint> pixelUserData)
|
||||||
|
{
|
||||||
|
if (!_transparentFillClearEnabled ||
|
||||||
|
textures.Count != 0 ||
|
||||||
|
vertexInputs.Count != 0 ||
|
||||||
|
pixelUserData.Count < 4 ||
|
||||||
|
renderState.Blend is not
|
||||||
|
{
|
||||||
|
Enable: true,
|
||||||
|
ColorSrcFactor: 1,
|
||||||
|
ColorDstFactor: 5,
|
||||||
|
ColorFunc: 0,
|
||||||
|
})
|
||||||
|
{
|
||||||
|
return renderState;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (var index = 0; index < 4; index++)
|
||||||
|
{
|
||||||
|
if ((pixelUserData[index] & 0x7FFF_FFFFu) != 0)
|
||||||
|
{
|
||||||
|
return renderState;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return renderState with
|
||||||
|
{
|
||||||
|
Blend = renderState.Blend with { Enable = false },
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
|
private static VulkanGuestIndexBuffer? CreateVulkanIndexBuffer(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
SubmittedDcbState state,
|
SubmittedDcbState state,
|
||||||
@@ -5635,4 +5775,86 @@ public static class AgcExports
|
|||||||
Console.Error.WriteLine(
|
Console.Error.WriteLine(
|
||||||
$"[LOADER][TRACE] agc.create_shader dst=0x{destinationAddress:X16} header=0x{headerAddress:X16} code=0x{codeAddress:X16} {detail}");
|
$"[LOADER][TRACE] agc.create_shader dst=0x{destinationAddress:X16} header=0x{headerAddress:X16} code=0x{codeAddress:X16} {detail}");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Packet layouts mirror what DcbWaitRegMem emits.
|
||||||
|
// 32-bit (ItNop/RWaitMem32): +4 addrLo, +8 addrHi, +12 mask, +16 cmp|op<<8, +20 ref.
|
||||||
|
// 64-bit (ItNop/RWaitMem64): +4 addrLo, +8 addrHi, +12 maskLo, +16 maskHi,
|
||||||
|
// +20 refLo, +24 refHi, +28 cmp|op<<8, +32 poll.
|
||||||
|
private static bool TryParseWaitRegMem(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong addr,
|
||||||
|
bool is64,
|
||||||
|
out ulong waitAddr,
|
||||||
|
out ulong refVal,
|
||||||
|
out ulong mask,
|
||||||
|
out uint cmpFunc)
|
||||||
|
{
|
||||||
|
waitAddr = refVal = mask = 0;
|
||||||
|
cmpFunc = 0;
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt32(addr + 4, out var lo) ||
|
||||||
|
!ctx.TryReadUInt32(addr + 8, out var hi))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
waitAddr = ((ulong)hi << 32) | lo;
|
||||||
|
if (!is64)
|
||||||
|
{
|
||||||
|
if (!ctx.TryReadUInt32(addr + 12, out var mask32) ||
|
||||||
|
!ctx.TryReadUInt32(addr + 16, out var cmpRaw) ||
|
||||||
|
!ctx.TryReadUInt32(addr + 20, out var refVal32))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mask = mask32;
|
||||||
|
refVal = refVal32;
|
||||||
|
cmpFunc = cmpRaw & 0x7;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt32(addr + 12, out var maskLo) ||
|
||||||
|
!ctx.TryReadUInt32(addr + 16, out var maskHi) ||
|
||||||
|
!ctx.TryReadUInt32(addr + 20, out var refLo) ||
|
||||||
|
!ctx.TryReadUInt32(addr + 24, out var refHi) ||
|
||||||
|
!ctx.TryReadUInt32(addr + 28, out var cmpRaw64))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
mask = ((ulong)maskHi << 32) | maskLo;
|
||||||
|
refVal = ((ulong)refHi << 32) | refLo;
|
||||||
|
cmpFunc = cmpRaw64 & 0x7;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Standard ItWaitRegMem (7 dwords, 32-bit compare):
|
||||||
|
// +4 cmp|(op&1)<<8, +8 addrLo, +12 addrHi, +16 ref, +20 mask, +24 poll.
|
||||||
|
private static bool TryParseStandardWaitRegMem(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong addr,
|
||||||
|
out ulong waitAddr,
|
||||||
|
out ulong refVal,
|
||||||
|
out ulong mask,
|
||||||
|
out uint cmpFunc)
|
||||||
|
{
|
||||||
|
waitAddr = refVal = mask = 0;
|
||||||
|
cmpFunc = 0;
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt32(addr + 4, out var cmpRaw) ||
|
||||||
|
!ctx.TryReadUInt32(addr + 8, out var lo) ||
|
||||||
|
!ctx.TryReadUInt32(addr + 12, out var hi) ||
|
||||||
|
!ctx.TryReadUInt32(addr + 16, out var reference) ||
|
||||||
|
!ctx.TryReadUInt32(addr + 20, out var mask32))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
cmpFunc = cmpRaw & 0x7;
|
||||||
|
waitAddr = ((ulong)hi << 32) | lo;
|
||||||
|
refVal = reference;
|
||||||
|
mask = mask32;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -718,7 +718,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
}
|
}
|
||||||
|
|
||||||
var src0 = word & 0x1FF;
|
var src0 = word & 0x1FF;
|
||||||
sizeDwords = opcode is 0x20 or 0x21 || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
sizeDwords = opcode is 0x20 or 0x21 or 0x2C or 0x2D || src0 is 0xF9 or 0xFA or 0xFF ? 2u : 1u;
|
||||||
error = string.Empty;
|
error = string.Empty;
|
||||||
name = opcode switch
|
name = opcode switch
|
||||||
{
|
{
|
||||||
@@ -757,7 +757,9 @@ internal static class Gen5ShaderTranslator
|
|||||||
0x28 => "VAddcU32",
|
0x28 => "VAddcU32",
|
||||||
0x29 => "VSubbU32",
|
0x29 => "VSubbU32",
|
||||||
0x2A => "VSubbrevU32",
|
0x2A => "VSubbrevU32",
|
||||||
0x2B => "VLdexpF32",
|
0x2B => "VFmacF32",
|
||||||
|
0x2C => "VFmamkF32",
|
||||||
|
0x2D => "VFmaakF32",
|
||||||
0x2F => "VCvtPkrtzF16F32",
|
0x2F => "VCvtPkrtzF16F32",
|
||||||
0x30 => "VCvtPkU16U32",
|
0x30 => "VCvtPkU16U32",
|
||||||
0x31 => "VCvtPkI16I32",
|
0x31 => "VCvtPkI16I32",
|
||||||
@@ -870,6 +872,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
0x10F => "VMinF32",
|
0x10F => "VMinF32",
|
||||||
0x110 => "VMaxF32",
|
0x110 => "VMaxF32",
|
||||||
0x11F => "VMacF32",
|
0x11F => "VMacF32",
|
||||||
|
0x12B => "VFmacF32",
|
||||||
0x12F => "VCvtPkrtzF16F32",
|
0x12F => "VCvtPkrtzF16F32",
|
||||||
0x141 => "VMadF32",
|
0x141 => "VMadF32",
|
||||||
0x143 => "VMadU32U24",
|
0x143 => "VMadU32U24",
|
||||||
@@ -897,6 +900,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
0x169 => "VMulLoU32",
|
0x169 => "VMulLoU32",
|
||||||
0x16A => "VMulHiU32",
|
0x16A => "VMulHiU32",
|
||||||
0x16B => "VMulLoI32",
|
0x16B => "VMulLoI32",
|
||||||
|
0x16C => "VMulHiI32",
|
||||||
0x360 => "VMadU32U16",
|
0x360 => "VMadU32U16",
|
||||||
0x361 => "VMulLoU32",
|
0x361 => "VMulLoU32",
|
||||||
0x362 => "VLdexpF32",
|
0x362 => "VLdexpF32",
|
||||||
@@ -1379,7 +1383,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
Gen5Operand.Source(word & 0x1FF, literal),
|
Gen5Operand.Source(word & 0x1FF, literal),
|
||||||
Gen5Operand.Vector((word >> 9) & 0xFF),
|
Gen5Operand.Vector((word >> 9) & 0xFF),
|
||||||
];
|
];
|
||||||
if (opcode == "VMadMkF32" && literal.HasValue)
|
if (opcode is "VMadMkF32" or "VFmamkF32" && literal.HasValue)
|
||||||
{
|
{
|
||||||
sources =
|
sources =
|
||||||
[
|
[
|
||||||
@@ -1388,7 +1392,7 @@ internal static class Gen5ShaderTranslator
|
|||||||
sources[1],
|
sources[1],
|
||||||
];
|
];
|
||||||
}
|
}
|
||||||
else if (opcode == "VMadAkF32" && literal.HasValue)
|
else if (opcode is "VMadAkF32" or "VFmaakF32" && literal.HasValue)
|
||||||
{
|
{
|
||||||
sources =
|
sources =
|
||||||
[
|
[
|
||||||
|
|||||||
@@ -255,6 +255,8 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
case "VFmaF32":
|
case "VFmaF32":
|
||||||
case "VMadMkF32":
|
case "VMadMkF32":
|
||||||
case "VMadAkF32":
|
case "VMadAkF32":
|
||||||
|
case "VFmamkF32":
|
||||||
|
case "VFmaakF32":
|
||||||
result = EmitFloatResult(
|
result = EmitFloatResult(
|
||||||
instruction,
|
instruction,
|
||||||
Ext(
|
Ext(
|
||||||
@@ -265,6 +267,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
GetFloatSource(instruction, 2)));
|
GetFloatSource(instruction, 2)));
|
||||||
break;
|
break;
|
||||||
case "VMacF32":
|
case "VMacF32":
|
||||||
|
case "VFmacF32":
|
||||||
{
|
{
|
||||||
var addend = Bitcast(_floatType, LoadV(destination));
|
var addend = Bitcast(_floatType, LoadV(destination));
|
||||||
result = EmitFloatResult(
|
result = EmitFloatResult(
|
||||||
@@ -334,6 +337,7 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
result = EmitSubtractWithBorrow(instruction, reverse: true);
|
result = EmitSubtractWithBorrow(instruction, reverse: true);
|
||||||
break;
|
break;
|
||||||
case "VMulLoU32":
|
case "VMulLoU32":
|
||||||
|
case "VMulLoI32":
|
||||||
case "VMulU32U24":
|
case "VMulU32U24":
|
||||||
result = EmitIntegerBinary(instruction, SpirvOp.IMul);
|
result = EmitIntegerBinary(instruction, SpirvOp.IMul);
|
||||||
break;
|
break;
|
||||||
@@ -369,6 +373,29 @@ internal static partial class Gen5SpirvTranslator
|
|||||||
_module.Constant64(_ulongType, 32)));
|
_module.Constant64(_ulongType, 32)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
|
case "VMulHiI32":
|
||||||
|
{
|
||||||
|
var wideLeft = _module.AddInstruction(
|
||||||
|
SpirvOp.SConvert,
|
||||||
|
_longType,
|
||||||
|
Bitcast(_intType, GetRawSource(instruction, 0)));
|
||||||
|
var wideRight = _module.AddInstruction(
|
||||||
|
SpirvOp.SConvert,
|
||||||
|
_longType,
|
||||||
|
Bitcast(_intType, GetRawSource(instruction, 1)));
|
||||||
|
var product = _module.AddInstruction(
|
||||||
|
SpirvOp.IMul,
|
||||||
|
_longType,
|
||||||
|
wideLeft,
|
||||||
|
wideRight);
|
||||||
|
result = _module.AddInstruction(
|
||||||
|
SpirvOp.UConvert,
|
||||||
|
_uintType,
|
||||||
|
ShiftRightLogical64(
|
||||||
|
Bitcast(_ulongType, product),
|
||||||
|
_module.Constant64(_ulongType, 32)));
|
||||||
|
break;
|
||||||
|
}
|
||||||
case "VMadU32U24":
|
case "VMadU32U24":
|
||||||
{
|
{
|
||||||
var left = BitwiseAnd(
|
var left = BitwiseAnd(
|
||||||
|
|||||||
@@ -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,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -267,19 +267,53 @@ public static class AmprExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!AmprFileRegistry.TryGetHostPath(fileId, out var hostPath))
|
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);
|
if (destination != 0 && size > 0)
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
{
|
||||||
|
int chunkSize = (int)Math.Min(size, 4096);
|
||||||
|
Span<byte> zeros = stackalloc byte[chunkSize];
|
||||||
|
zeros.Clear();
|
||||||
|
while (bytesRead < size)
|
||||||
|
{
|
||||||
|
int currentChunk = (int)Math.Min((ulong)chunkSize, size - bytesRead);
|
||||||
|
if (!ctx.Memory.TryWrite(destination + bytesRead, zeros[..currentChunk]))
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
bytesRead += (ulong)currentChunk;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, "(missing)", (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out var bytesRead);
|
// Offset -1 means "continue after the previous read of this file id".
|
||||||
|
if (fileOffset == unchecked((ulong)(long)-1))
|
||||||
|
{
|
||||||
|
fileOffset = PakDirectoryTracker.ResolveSequentialOffset(fileId, size);
|
||||||
|
}
|
||||||
|
else if (fileOffset > long.MaxValue)
|
||||||
|
{
|
||||||
|
fileOffset = 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
var result = TryReadFileToGuestMemory(ctx, hostPath, fileOffset, destination, size, out bytesRead);
|
||||||
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
TraceAmprRead(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead, hostPath, result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
PakDirectoryTracker.OnReadCompleted(ctx, fileId, destination, fileOffset, bytesRead);
|
||||||
|
|
||||||
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
if (!AppendReadFileRecord(ctx, commandBuffer, fileId, destination, size, fileOffset, bytesRead))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ internal static class AmprFileRegistry
|
|||||||
return _hostPathsById.TryGetValue(id, out hostPath!);
|
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);
|
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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -8,9 +8,14 @@ namespace SharpEmu.Libs.Audio;
|
|||||||
|
|
||||||
public static class AudioOut2Exports
|
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 AudioOut2ContextMemorySize = 0x10000;
|
||||||
private const int AudioOut2ContextMemoryAlignment = 0x10000;
|
|
||||||
private static long _nextContextHandle = 1;
|
private static long _nextContextHandle = 1;
|
||||||
private static long _nextUserHandle = 1;
|
private static long _nextUserHandle = 1;
|
||||||
private static int _nextPortId;
|
private static int _nextPortId;
|
||||||
@@ -59,20 +64,13 @@ public static class AudioOut2Exports
|
|||||||
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
public static int AudioOut2ContextQueryMemory(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var paramAddress = ctx[CpuRegister.Rdi];
|
var paramAddress = ctx[CpuRegister.Rdi];
|
||||||
var memoryInfoAddress = ctx[CpuRegister.Rsi];
|
var outMemorySizeAddress = ctx[CpuRegister.Rsi];
|
||||||
if (paramAddress == 0 || memoryInfoAddress == 0)
|
if (paramAddress == 0 || outMemorySizeAddress == 0)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> memoryInfo = stackalloc byte[0x20];
|
return ctx.TryWriteUInt64(outMemorySizeAddress, AudioOut2ContextMemorySize)
|
||||||
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)
|
|
||||||
? ctx.SetReturn(0)
|
? ctx.SetReturn(0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,42 @@ public static class AvPlayerExports
|
|||||||
return unchecked((int)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(
|
[SysAbiExport(
|
||||||
Nid = "HD1YKVU26-M",
|
Nid = "HD1YKVU26-M",
|
||||||
ExportName = "sceAvPlayerPostInit",
|
ExportName = "sceAvPlayerPostInit",
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ namespace SharpEmu.Libs.CommonDialog;
|
|||||||
|
|
||||||
public static class CommonDialogExports
|
public static class CommonDialogExports
|
||||||
{
|
{
|
||||||
private const int AlreadySystemInitialized = unchecked((int)0x80B80002);
|
|
||||||
private static int _initialized;
|
private static int _initialized;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -18,11 +17,12 @@ public static class CommonDialogExports
|
|||||||
LibraryName = "libSceCommonDialog")]
|
LibraryName = "libSceCommonDialog")]
|
||||||
public static int CommonDialogInitialize(CpuContext ctx)
|
public static int CommonDialogInitialize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
// Games can initialize the common-dialog service defensively from more than one
|
||||||
? 0
|
// subsystem. Keeping this HLE initialization idempotent avoids turning an error
|
||||||
: AlreadySystemInitialized;
|
// reporting path into a second, unrelated failure.
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
Interlocked.Exchange(ref _initialized, 1);
|
||||||
return result;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
|
|||||||
@@ -8,8 +8,13 @@ namespace SharpEmu.Libs.CommonDialog;
|
|||||||
|
|
||||||
public static class MsgDialogExports
|
public static class MsgDialogExports
|
||||||
{
|
{
|
||||||
private const int AlreadyInitialized = unchecked((int)0x80B80002);
|
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 _initialized;
|
||||||
|
private static int _status;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "lDqxaY1UbEo",
|
Nid = "lDqxaY1UbEo",
|
||||||
@@ -18,9 +23,162 @@ public static class MsgDialogExports
|
|||||||
LibraryName = "libSceMsgDialog")]
|
LibraryName = "libSceMsgDialog")]
|
||||||
public static int MsgDialogInitialize(CpuContext ctx)
|
public static int MsgDialogInitialize(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var result = Interlocked.Exchange(ref _initialized, 1) == 0
|
// Treat repeated initialization as success. The dialog service is process-global in
|
||||||
? 0
|
// this HLE implementation and has no per-call resources to recreate.
|
||||||
: AlreadyInitialized;
|
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);
|
ctx[CpuRegister.Rax] = unchecked((ulong)result);
|
||||||
return result;
|
return result;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -93,7 +93,7 @@ public static class CxaGuardExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "",
|
Nid = "9rAeANT2tyE",
|
||||||
ExportName = "__cxa_guard_release",
|
ExportName = "__cxa_guard_release",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libc")]
|
LibraryName = "libc")]
|
||||||
@@ -142,7 +142,7 @@ public static class CxaGuardExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "",
|
Nid = "2emaaluWzUw",
|
||||||
ExportName = "__cxa_guard_abort",
|
ExportName = "__cxa_guard_abort",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libc")]
|
LibraryName = "libc")]
|
||||||
|
|||||||
@@ -0,0 +1,104 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.DiscMap;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// HLE implementation of libSceDiscMap, ported from Kyty's LibDiscMap.cpp
|
||||||
|
/// (https://github.com/InoriRus/Kyty, MIT). Titles installed to internal
|
||||||
|
/// storage query these entry points on nearly every file access to decide
|
||||||
|
/// whether a read must be redirected to the Blu-ray drive. Reporting every
|
||||||
|
/// request as already resident on HDD lets the regular file system path
|
||||||
|
/// service the read.
|
||||||
|
/// </summary>
|
||||||
|
public static class DiscMapExports
|
||||||
|
{
|
||||||
|
private const int DiscMapErrorInvalidArgument = unchecked((int)0x81100001);
|
||||||
|
private const int DiscMapErrorLocationNotMapped = unchecked((int)0x81100002);
|
||||||
|
private const int DiscMapErrorFileNotFound = unchecked((int)0x81100003);
|
||||||
|
private const int DiscMapErrorNoBitmapInfo = unchecked((int)0x81100004);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "lbQKqsERhtE",
|
||||||
|
ExportName = "sceDiscMapIsRequestOnHDD",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceDiscMap")]
|
||||||
|
public static int DiscMapIsRequestOnHDD(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var pathAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var offset = ctx[CpuRegister.Rsi];
|
||||||
|
var size = ctx[CpuRegister.Rdx];
|
||||||
|
var resultAddress = ctx[CpuRegister.Rcx];
|
||||||
|
|
||||||
|
if (pathAddress == 0 || resultAddress == 0)
|
||||||
|
{
|
||||||
|
return DiscMapErrorInvalidArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteInt32(resultAddress, 1))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceDiscMap(ctx, "sceDiscMapIsRequestOnHDD", pathAddress, offset, size);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "fJgP+wqifno",
|
||||||
|
ExportName = "sceDiscMapUnknownFJgP",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceDiscMap")]
|
||||||
|
public static int DiscMapUnknownFJgP(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownFJgP");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "ioKMruft1ek",
|
||||||
|
ExportName = "sceDiscMapUnknownIoKM",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceDiscMap")]
|
||||||
|
public static int DiscMapUnknownIoKM(CpuContext ctx) => WriteMappingTriple(ctx, "sceDiscMapUnknownIoKM");
|
||||||
|
|
||||||
|
private static int WriteMappingTriple(CpuContext ctx, string exportName)
|
||||||
|
{
|
||||||
|
var pathAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var offset = ctx[CpuRegister.Rsi];
|
||||||
|
var size = ctx[CpuRegister.Rdx];
|
||||||
|
var flagsAddress = ctx[CpuRegister.Rcx];
|
||||||
|
var outAddress1 = ctx[CpuRegister.R8];
|
||||||
|
var outAddress2 = ctx[CpuRegister.R9];
|
||||||
|
|
||||||
|
if (pathAddress == 0 || flagsAddress == 0 || outAddress1 == 0 || outAddress2 == 0)
|
||||||
|
{
|
||||||
|
return DiscMapErrorInvalidArgument;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(flagsAddress, 0) ||
|
||||||
|
!ctx.TryWriteUInt64(outAddress1, 0) ||
|
||||||
|
!ctx.TryWriteUInt64(outAddress2, 0))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
TraceDiscMap(ctx, exportName, pathAddress, offset, size);
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void TraceDiscMap(CpuContext ctx, string exportName, ulong pathAddress, ulong offset, ulong size)
|
||||||
|
{
|
||||||
|
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCMAP"), "1", StringComparison.Ordinal))
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var path = ctx.TryReadNullTerminatedUtf8(pathAddress, 1024, out var value)
|
||||||
|
? value
|
||||||
|
: $"<unreadable 0x{pathAddress:X16}>";
|
||||||
|
Console.Error.WriteLine($"[HLE][DISCMAP] {exportName} path={path} offset=0x{offset:X} size=0x{size:X}");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -149,6 +149,19 @@ public static class KernelAprCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Success stub: the argument layout is unknown and callers tolerate the
|
||||||
|
// empty answer (Quake streams fine), so no output payload is written until
|
||||||
|
// the real signature is reversed.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "WvEu7yl3Ivg",
|
||||||
|
ExportName = "sceKernelAprGetFileSize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelAprGetFileSize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(0);
|
||||||
|
}
|
||||||
|
|
||||||
private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress)
|
private static bool TryWriteAprResult(CpuContext ctx, ulong resultAddress)
|
||||||
{
|
{
|
||||||
Span<byte> result = stackalloc byte[sizeof(ulong)];
|
Span<byte> result = stackalloc byte[sizeof(ulong)];
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ public static class KernelExports
|
|||||||
private static readonly object _coredumpGate = new();
|
private static readonly object _coredumpGate = new();
|
||||||
private static ulong _coredumpHandler;
|
private static ulong _coredumpHandler;
|
||||||
private static ulong _coredumpHandlerContext;
|
private static ulong _coredumpHandlerContext;
|
||||||
|
private const uint Gen4CompiledSdkVersion = 0x05000000;
|
||||||
|
private const uint Gen5CompiledSdkVersion = 0x09000000;
|
||||||
|
|
||||||
private readonly record struct CxaDestructorEntry(
|
private readonly record struct CxaDestructorEntry(
|
||||||
ulong Function,
|
ulong Function,
|
||||||
@@ -26,7 +28,24 @@ public static class KernelExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
public static int KernelGetCompiledSdkVersion(CpuContext ctx)
|
||||||
{
|
{
|
||||||
_ = ctx;
|
var versionAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (versionAddress == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var sdkVersion = ctx.TargetGeneration == Generation.Gen5
|
||||||
|
? Gen5CompiledSdkVersion
|
||||||
|
: Gen4CompiledSdkVersion;
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt32(versionAddress, sdkVersion))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -52,12 +71,20 @@ public static class KernelExports
|
|||||||
ExportName = "exit",
|
ExportName = "exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int Exit(CpuContext ctx)
|
public static int Exit(CpuContext ctx) => RequestProcessExit(ctx, "exit");
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "L1SBTkC+Cvw",
|
||||||
|
ExportName = "abort",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Abort(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
// Route through the same graceful guest-entry-exit path as exit(): letting the call
|
||||||
Console.Error.WriteLine($"[LOADER][INFO] exit(status={status})");
|
// fall through to the host's native abort() does not unwind the guest thread cleanly.
|
||||||
GuestThreadExecution.RequestCurrentEntryExit("exit", status);
|
Console.Error.WriteLine("[LOADER][INFO] abort() called by guest - terminating");
|
||||||
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
GuestThreadExecution.RequestCurrentEntryExit("abort", -1);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(-1L));
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -172,11 +199,7 @@ public static class KernelExports
|
|||||||
ExportName = "_exit",
|
ExportName = "_exit",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int UnderscoreExit(CpuContext ctx)
|
public static int UnderscoreExit(CpuContext ctx) => RequestProcessExit(ctx, "_exit");
|
||||||
{
|
|
||||||
_ = ctx;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Ac86z8q7T8A",
|
Nid = "Ac86z8q7T8A",
|
||||||
@@ -255,20 +278,14 @@ public static class KernelExports
|
|||||||
ExportName = "pthread_create",
|
ExportName = "pthread_create",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCreate(CpuContext ctx)
|
public static int PosixPthreadCreate(CpuContext ctx) => PthreadCreate(ctx);
|
||||||
{
|
|
||||||
return PthreadCreate(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "Jmi+9w9u0E4",
|
Nid = "Jmi+9w9u0E4",
|
||||||
ExportName = "pthread_create_name_np",
|
ExportName = "pthread_create_name_np",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCreateNameNp(CpuContext ctx)
|
public static int PosixPthreadCreateNameNp(CpuContext ctx) => PthreadCreate(ctx);
|
||||||
{
|
|
||||||
return PthreadCreate(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "3kg7rT0NQIs",
|
Nid = "3kg7rT0NQIs",
|
||||||
@@ -345,10 +362,7 @@ public static class KernelExports
|
|||||||
ExportName = "pthread_join",
|
ExportName = "pthread_join",
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadJoin(CpuContext ctx)
|
public static int PosixPthreadJoin(CpuContext ctx) => PthreadJoin(ctx);
|
||||||
{
|
|
||||||
return PthreadJoin(ctx);
|
|
||||||
}
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "wuCroIGjt2g",
|
Nid = "wuCroIGjt2g",
|
||||||
@@ -437,4 +451,76 @@ public static class KernelExports
|
|||||||
{
|
{
|
||||||
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
return string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int RequestProcessExit(CpuContext ctx, string syscallName)
|
||||||
|
{
|
||||||
|
var status = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] {syscallName}(status={status})");
|
||||||
|
GuestThreadExecution.RequestCurrentEntryExit(syscallName, status);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)status);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Unidentified kernel NIDs observed at runtime; stubbed to return 0 until they are
|
||||||
|
// resolved against scripts/ps5_names.txt.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "9T2pDF2Ryqg",
|
||||||
|
ExportName = "sceKernelUnknown9T2p",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelUnknown9T2p(CpuContext ctx) => SetZeroReturn(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "TU-d9PfIHPM",
|
||||||
|
ExportName = "sceKernelUnknownTU",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelUnknownTU(CpuContext ctx) => SetZeroReturn(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "iWQWrwiSt8A",
|
||||||
|
ExportName = "sceKernelUnknowniWQW",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelUnknowniWQW(CpuContext ctx) => SetZeroReturn(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "KuOmgKoqCdY",
|
||||||
|
ExportName = "sceKernelUnknownKuOmg",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelUnknownKuOmg(CpuContext ctx) => SetZeroReturn(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "RenI1lL1WFk",
|
||||||
|
ExportName = "sceKernelUnknownRenI",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelUnknownRenI(CpuContext ctx) => SetZeroReturn(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "pQGpHYopAIY",
|
||||||
|
ExportName = "sceKernelUnknownpQGp",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelUnknownpQGp(CpuContext ctx) => SetZeroReturn(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Rbvt+5Y2iEw",
|
||||||
|
ExportName = "sceKernelUnknownRbvt",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelUnknownRbvt(CpuContext ctx) => SetZeroReturn(ctx);
|
||||||
|
|
||||||
|
// NOTE: "SHtAad20YYM"/"DIxvoy7Ngvk" are sce::Json::Value::getType/getInteger, and
|
||||||
|
// "3GPpjQdAMTw"/"9rAeANT2tyE"/"tsvEmnenz48" are __cxa_guard_acquire/__cxa_guard_release/
|
||||||
|
// __cxa_atexit (verified by hashing against scripts/ps5_names.txt). Do not register them
|
||||||
|
// here as kernel mutex functions: the cxa guards are implemented in CxxAbiExports.cs and
|
||||||
|
// shadowing them breaks every C++ static-init guard in the game.
|
||||||
|
|
||||||
|
private static int SetZeroReturn(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return 0;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -53,6 +53,8 @@ public static class KernelMemoryCompatExports
|
|||||||
private const int OrbisVirtualQueryInfoSize = 72;
|
private const int OrbisVirtualQueryInfoSize = 72;
|
||||||
private const int OrbisKernelMaximumNameLength = 32;
|
private const int OrbisKernelMaximumNameLength = 32;
|
||||||
private const uint MemCommit = 0x1000;
|
private const uint MemCommit = 0x1000;
|
||||||
|
private const uint MemReserve = 0x2000;
|
||||||
|
private const uint MemRelease = 0x8000;
|
||||||
private const uint HostPageNoAccess = 0x01;
|
private const uint HostPageNoAccess = 0x01;
|
||||||
private const uint HostPageReadOnly = 0x02;
|
private const uint HostPageReadOnly = 0x02;
|
||||||
private const uint HostPageReadWrite = 0x04;
|
private const uint HostPageReadWrite = 0x04;
|
||||||
@@ -121,6 +123,7 @@ public static class KernelMemoryCompatExports
|
|||||||
private static int _hostMemoryWriteFallbackCount;
|
private static int _hostMemoryWriteFallbackCount;
|
||||||
private static int _hostMemoryReadFallbackCount;
|
private static int _hostMemoryReadFallbackCount;
|
||||||
private static int _nullWcscpyRecoveryCount;
|
private static int _nullWcscpyRecoveryCount;
|
||||||
|
private static int _nullStrcasecmpRecoveryCount;
|
||||||
private static string? _cachedApp0Root;
|
private static string? _cachedApp0Root;
|
||||||
private static string? _cachedDownload0Root;
|
private static string? _cachedDownload0Root;
|
||||||
|
|
||||||
@@ -143,6 +146,13 @@ public static class KernelMemoryCompatExports
|
|||||||
[return: MarshalAs(UnmanagedType.Bool)]
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
private static extern bool VirtualProtect(nint lpAddress, nuint dwSize, uint flNewProtect, out uint lpflOldProtect);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
private static extern nint VirtualAlloc(nint lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||||
|
|
||||||
|
[DllImport("kernel32.dll", SetLastError = true)]
|
||||||
|
[return: MarshalAs(UnmanagedType.Bool)]
|
||||||
|
private static extern bool VirtualFree(nint lpAddress, nuint dwSize, uint dwFreeType);
|
||||||
|
|
||||||
private sealed class OpenDirectory
|
private sealed class OpenDirectory
|
||||||
{
|
{
|
||||||
public required string Path { get; init; }
|
public required string Path { get; init; }
|
||||||
@@ -151,7 +161,7 @@ public static class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct DirectAllocation(ulong Start, ulong Length, int MemoryType);
|
private readonly record struct DirectAllocation(ulong Start, ulong Length, int MemoryType);
|
||||||
private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment);
|
private readonly record struct LibcHeapAllocation(nint BaseAddress, nuint Size, nuint Alignment, bool IsGuarded);
|
||||||
private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, bool IsDirect, ulong DirectStart);
|
private readonly record struct MappedRegion(ulong Address, ulong Length, int Protection, bool IsFlexible, bool IsDirect, ulong DirectStart);
|
||||||
private readonly record struct BatchMapEntry(ulong Start, ulong Offset, ulong Length, byte Protection, byte Type, int Operation);
|
private readonly record struct BatchMapEntry(ulong Start, ulong Offset, ulong Length, byte Protection, byte Type, int Operation);
|
||||||
|
|
||||||
@@ -186,6 +196,14 @@ public static class KernelMemoryCompatExports
|
|||||||
ulong length,
|
ulong length,
|
||||||
ulong alignment,
|
ulong alignment,
|
||||||
out ulong address)
|
out ulong address)
|
||||||
|
=> TryAllocateHleData(ctx, length, alignment, OrbisProtCpuReadWrite, out address);
|
||||||
|
|
||||||
|
internal static bool TryAllocateHleData(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong length,
|
||||||
|
ulong alignment,
|
||||||
|
int protection,
|
||||||
|
out ulong address)
|
||||||
{
|
{
|
||||||
address = 0;
|
address = 0;
|
||||||
if (length == 0 || length > int.MaxValue)
|
if (length == 0 || length > int.MaxValue)
|
||||||
@@ -200,7 +218,7 @@ public static class KernelMemoryCompatExports
|
|||||||
var desiredAddress = AlignUp(
|
var desiredAddress = AlignUp(
|
||||||
_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress,
|
_nextVirtualAddress == 0 ? 0x1_0000_0000UL : _nextVirtualAddress,
|
||||||
effectiveAlignment);
|
effectiveAlignment);
|
||||||
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, OrbisProtCpuReadWrite, effectiveAlignment, out address) ||
|
if (!TryReserveGuestVirtualRange(ctx, desiredAddress, mappedLength, protection, effectiveAlignment, out address) ||
|
||||||
address == 0)
|
address == 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -210,7 +228,7 @@ public static class KernelMemoryCompatExports
|
|||||||
_mappedRegions[address] = new MappedRegion(
|
_mappedRegions[address] = new MappedRegion(
|
||||||
address,
|
address,
|
||||||
mappedLength,
|
mappedLength,
|
||||||
OrbisProtCpuReadWrite,
|
protection,
|
||||||
IsFlexible: false,
|
IsFlexible: false,
|
||||||
IsDirect: false,
|
IsDirect: false,
|
||||||
DirectStart: 0);
|
DirectStart: 0);
|
||||||
@@ -231,6 +249,69 @@ public static class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static ulong _dummyVtableAddress;
|
||||||
|
private const int DummyVtableSlotCount = 64;
|
||||||
|
|
||||||
|
// Some KexEngine objects (mutex wrappers, NGS2 system/rack/voice handles, etc.) are treated
|
||||||
|
// by guest code as C++ instances with a vtable pointer at offset 0. When an HLE constructor
|
||||||
|
// leaves that slot zeroed, the game's own virtual dispatch (call [ [obj]+N ]) reads a null
|
||||||
|
// vtable and jumps through address N, crashing (e.g. "CALL qword ptr [RAX+0x58]" with RAX=0).
|
||||||
|
// This allocates one shared, executable no-op stub plus a vtable of pointers to it, so any
|
||||||
|
// HLE object constructor can make virtual calls on its output land safely.
|
||||||
|
internal static bool TryWriteDummyVtable(CpuContext ctx, ulong objectAddress)
|
||||||
|
{
|
||||||
|
if (objectAddress == 0 || !TryEnsureDummyVtable(ctx, out var vtableAddress))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ctx.TryWriteUInt64(objectAddress, vtableAddress);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryEnsureDummyVtable(CpuContext ctx, out ulong vtableAddress)
|
||||||
|
{
|
||||||
|
lock (_memoryGate)
|
||||||
|
{
|
||||||
|
if (_dummyVtableAddress != 0)
|
||||||
|
{
|
||||||
|
vtableAddress = _dummyVtableAddress;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
const int executableReadWrite = OrbisProtCpuRead | OrbisProtCpuWrite | OrbisProtCpuExec;
|
||||||
|
if (!TryAllocateHleData(ctx, 0x1000, 0x1000, executableReadWrite, out var block))
|
||||||
|
{
|
||||||
|
vtableAddress = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// xor eax, eax; ret - every dummy virtual method just returns 0 and does nothing else.
|
||||||
|
if (!ctx.Memory.TryWrite(block, new byte[] { 0x31, 0xC0, 0xC3 }))
|
||||||
|
{
|
||||||
|
vtableAddress = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var table = new byte[DummyVtableSlotCount * sizeof(ulong)];
|
||||||
|
for (var i = 0; i < DummyVtableSlotCount; i++)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(table.AsSpan(i * sizeof(ulong), sizeof(ulong)), block);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tableAddress = block + 0x100;
|
||||||
|
if (!ctx.Memory.TryWrite(tableAddress, table))
|
||||||
|
{
|
||||||
|
vtableAddress = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine($"[LOADER][INFO] Dummy vtable ready: stub=0x{block:X16} vtable=0x{tableAddress:X16} slots={DummyVtableSlotCount}");
|
||||||
|
_dummyVtableAddress = tableAddress;
|
||||||
|
vtableAddress = tableAddress;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static void RegisterReservedVirtualRange(ulong address, ulong length)
|
internal static void RegisterReservedVirtualRange(ulong address, ulong length)
|
||||||
{
|
{
|
||||||
if (address == 0 || length == 0)
|
if (address == 0 || length == 0)
|
||||||
@@ -281,6 +362,18 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Longer null-dst memsets are unrecoverable, but RAX must still be set - leaving it
|
||||||
|
// stale here previously let callers that do `buf = memset(...)` carry on with a
|
||||||
|
// garbage "buffer" pointer instead of a clean NULL, causing a *different*,
|
||||||
|
// confusingly-located crash further downstream.
|
||||||
|
var largeRecoveryIndex = Interlocked.Increment(ref _nullMemsetRecoveryCount);
|
||||||
|
if (largeRecoveryIndex <= 8)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARNING] memset null-dst (len>0x20) recovery#{largeRecoveryIndex}: rip=0x{ctx.Rip:X16} len=0x{length:X} val=0x{value:X2}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -304,6 +397,7 @@ public static class KernelMemoryCompatExports
|
|||||||
Console.WriteLine("!!! CRITICAL: Bad Memset Call !!!");
|
Console.WriteLine("!!! CRITICAL: Bad Memset Call !!!");
|
||||||
Console.WriteLine($"Called from RIP: 0x{ctx.Rip:X}");
|
Console.WriteLine($"Called from RIP: 0x{ctx.Rip:X}");
|
||||||
Console.WriteLine($"dst=0x{destination:X} val=0x{value:X2} len=0x{length:X}");
|
Console.WriteLine($"dst=0x{destination:X} val=0x{value:X2} len=0x{length:X}");
|
||||||
|
ctx[CpuRegister.Rax] = destination;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -536,6 +630,80 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "AV6ipCNa4Rw",
|
||||||
|
ExportName = "strcasecmp",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Strcasecmp(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var left = ctx[CpuRegister.Rdi];
|
||||||
|
var right = ctx[CpuRegister.Rsi];
|
||||||
|
if (left == 0 || right == 0)
|
||||||
|
{
|
||||||
|
var recoveryIndex = Interlocked.Increment(ref _nullStrcasecmpRecoveryCount);
|
||||||
|
if (recoveryIndex <= 16)
|
||||||
|
{
|
||||||
|
var otherAddress = left == 0 ? right : left;
|
||||||
|
var otherText = otherAddress != 0 && TryReadNullTerminatedUtf8(ctx, otherAddress, 256, out var text)
|
||||||
|
? text
|
||||||
|
: "<unreadable>";
|
||||||
|
_ = ctx.TryReadUInt64(ctx[CpuRegister.Rsp], out var returnRip);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARNING] strcasecmp null-arg recovery#{recoveryIndex}: ret=0x{returnRip:X16} left=0x{left:X16} right=0x{right:X16} other=\"{otherText}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Real strcasecmp(NULL, x) is undefined behaviour and previously crashed inside the
|
||||||
|
// LLE-routed implementation. Treat it as "not equal" instead so callers doing
|
||||||
|
// `if (strcasecmp(a, b) == 0)` degrade gracefully rather than taking down the guest.
|
||||||
|
ctx[CpuRegister.Rax] = left == right ? 0uL : 1uL;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryCompareStringsCaseInsensitive(ctx, left, right, limit: ulong.MaxValue, out var compare))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 1;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)compare);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryCompareStringsCaseInsensitive(CpuContext ctx, ulong left, ulong right, ulong limit, out int compare)
|
||||||
|
{
|
||||||
|
compare = 0;
|
||||||
|
if (left == 0 || right == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var max = limit == ulong.MaxValue ? 1_048_576UL : Math.Min(limit, 1_048_576UL);
|
||||||
|
Span<byte> leftByte = stackalloc byte[1];
|
||||||
|
Span<byte> rightByte = stackalloc byte[1];
|
||||||
|
for (ulong i = 0; i < max; i++)
|
||||||
|
{
|
||||||
|
if (!TryReadCompat(ctx, left + i, leftByte) ||
|
||||||
|
!TryReadCompat(ctx, right + i, rightByte))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var leftLower = ToAsciiLower(leftByte[0]);
|
||||||
|
var rightLower = ToAsciiLower(rightByte[0]);
|
||||||
|
compare = leftLower - rightLower;
|
||||||
|
if (compare != 0 || leftByte[0] == 0 || rightByte[0] == 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
compare = 0;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte ToAsciiLower(byte value) => value is >= (byte)'A' and <= (byte)'Z' ? (byte)(value + 32) : value;
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "0nV21JjYCH8",
|
Nid = "0nV21JjYCH8",
|
||||||
ExportName = "wcsncpy",
|
ExportName = "wcsncpy",
|
||||||
@@ -587,6 +755,54 @@ public static class KernelMemoryCompatExports
|
|||||||
return VsnprintfCore(ctx);
|
return VsnprintfCore(ctx);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// sprintf/vsprintf are served from the same HLE formatting engine as snprintf/vsnprintf
|
||||||
|
// instead of falling through to the game's bundled libc.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "tcVi5SivF7Q",
|
||||||
|
ExportName = "sprintf",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Sprintf(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destination = ctx[CpuRegister.Rdi];
|
||||||
|
var formatAddress = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var format = Encoding.UTF8.GetString(formatBytes);
|
||||||
|
var rendered = FormatStringFromVarArgs(ctx, format, firstGpArgIndex: 2);
|
||||||
|
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, rendered);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "jbz9I9vkqkk",
|
||||||
|
ExportName = "vsprintf",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Vsprintf(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destination = ctx[CpuRegister.Rdi];
|
||||||
|
var formatAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var vaListAddress = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
if (!TryReadCString(ctx, formatAddress, 1_048_576, out var formatBytes))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var format = Encoding.UTF8.GetString(formatBytes);
|
||||||
|
if (!TryCreateVaListCursor(ctx, vaListAddress, out var vaCursor))
|
||||||
|
{
|
||||||
|
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, formatBytes);
|
||||||
|
}
|
||||||
|
|
||||||
|
var rendered = FormatString(ctx, format, ref vaCursor);
|
||||||
|
return WriteSnprintfOutput(ctx, destination, ulong.MaxValue, rendered);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "nJz16JE1txM",
|
Nid = "nJz16JE1txM",
|
||||||
ExportName = "swprintf",
|
ExportName = "swprintf",
|
||||||
@@ -945,15 +1161,27 @@ public static class KernelMemoryCompatExports
|
|||||||
{
|
{
|
||||||
var destination = ctx[CpuRegister.Rdi];
|
var destination = ctx[CpuRegister.Rdi];
|
||||||
var source = ctx[CpuRegister.Rsi];
|
var source = ctx[CpuRegister.Rsi];
|
||||||
var count = (int)Math.Min(ctx[CpuRegister.Rdx], int.MaxValue);
|
var rawCount = ctx[CpuRegister.Rdx];
|
||||||
if (count < 0)
|
|
||||||
|
// A garbage/absurd count (observed as e.g. 0xA7560035 from the same still-unidentified
|
||||||
|
// upstream bug that also feeds bad lengths to memset) must not reach
|
||||||
|
// GC.AllocateUninitializedArray: attempting a multi-GB allocation from a guest-thread
|
||||||
|
// call context corrupted the CLR outright ("Invalid Program: attempted to call a
|
||||||
|
// UnmanagedCallersOnly method from managed code") instead of throwing a normal
|
||||||
|
// exception. Reject anything above a sane bound before allocating.
|
||||||
|
const ulong maxSaneCount = 512UL * 1024 * 1024;
|
||||||
|
if (rawCount > maxSaneCount)
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][WARNING] memcpy oversized count rejected: dst=0x{destination:X16} src=0x{source:X16} count=0x{rawCount:X}");
|
||||||
|
ctx[CpuRegister.Rax] = destination;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var count = (int)rawCount;
|
||||||
var payload = GC.AllocateUninitializedArray<byte>(count);
|
var payload = GC.AllocateUninitializedArray<byte>(count);
|
||||||
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
if (count > 0 && (!TryReadCompat(ctx, source, payload) || !TryWriteCompat(ctx, destination, payload)))
|
||||||
{
|
{
|
||||||
|
ctx[CpuRegister.Rax] = destination;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1196,6 +1424,24 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Was unresolved (returning the 0x80020002 sentinel, then crashing when the guest
|
||||||
|
// dereferenced it) - the game's own heap instrumentation calls this hook when it
|
||||||
|
// detects a corrupted/invalid block, not the emulator's allocator, so this is purely
|
||||||
|
// a diagnostic sink: log what was reported and return success so the caller continues.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "al3JzFI9MQ0",
|
||||||
|
ExportName = "sceLibcInternalHeapErrorReportForGame",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceLibcInternal")]
|
||||||
|
public static int LibcInternalHeapErrorReportForGame(CpuContext ctx)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] sceLibcInternalHeapErrorReportForGame: rdi=0x{ctx[CpuRegister.Rdi]:X16} " +
|
||||||
|
$"rsi=0x{ctx[CpuRegister.Rsi]:X16} rdx=0x{ctx[CpuRegister.Rdx]:X16} rcx=0x{ctx[CpuRegister.Rcx]:X16}");
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "DfivPArhucg",
|
Nid = "DfivPArhucg",
|
||||||
ExportName = "memcmp",
|
ExportName = "memcmp",
|
||||||
@@ -1443,15 +1689,15 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var entryCount = (int)count;
|
||||||
|
Span<uint> localIds = count <= 256 ? stackalloc uint[entryCount] : new uint[entryCount];
|
||||||
|
Span<ulong> localSizes = count <= 128 ? stackalloc ulong[entryCount] : new ulong[entryCount];
|
||||||
|
var resolvedGuestPaths = new string[entryCount];
|
||||||
|
var resolvedHostPaths = new string[entryCount];
|
||||||
|
|
||||||
for (ulong i = 0; i < count; i++)
|
for (ulong i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
if (idsAddress != 0 &&
|
var index = (int)i;
|
||||||
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), uint.MaxValue))
|
|
||||||
{
|
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var guestPath))
|
if (!TryResolveAprFilepath(ctx, pathListAddress, i, out var guestPath))
|
||||||
{
|
{
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
||||||
@@ -1466,23 +1712,47 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
var fileId = AmprFileRegistry.Register(guestPath, hostPath);
|
var fileId = AmprFileRegistry.ComputeFileId(guestPath);
|
||||||
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}");
|
LogIoTrace("apr_resolve", guestPath, $"host='{hostPath}' index={i} count={count} id=0x{fileId:X8} size={fileSize}");
|
||||||
|
|
||||||
if (idsAddress != 0 &&
|
localIds[index] = fileId;
|
||||||
!TryWriteUInt32Compat(ctx, idsAddress + (i * sizeof(uint)), fileId))
|
localSizes[index] = fileSize;
|
||||||
|
resolvedGuestPaths[index] = guestPath;
|
||||||
|
resolvedHostPaths[index] = hostPath;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> sizePayload = count <= 64 ? stackalloc byte[entryCount * sizeof(ulong)] : new byte[entryCount * sizeof(ulong)];
|
||||||
|
for (ulong i = 0; i < count; i++)
|
||||||
|
{
|
||||||
|
BinaryPrimitives.WriteUInt64LittleEndian(sizePayload[(int)(i * sizeof(ulong))..], localSizes[(int)i]);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteCompat(ctx, sizesAddress, sizePayload))
|
||||||
|
{
|
||||||
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (idsAddress != 0)
|
||||||
|
{
|
||||||
|
Span<byte> idPayload = count <= 128 ? stackalloc byte[entryCount * sizeof(uint)] : new byte[entryCount * sizeof(uint)];
|
||||||
|
for (ulong i = 0; i < count; i++)
|
||||||
{
|
{
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
BinaryPrimitives.WriteUInt32LittleEndian(idPayload[(int)(i * sizeof(uint))..], localIds[(int)i]);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryWriteUInt64Compat(ctx, sizesAddress + (i * sizeof(ulong)), fileSize))
|
if (!TryWriteCompat(ctx, idsAddress, idPayload))
|
||||||
{
|
{
|
||||||
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
for (var i = 0; i < entryCount; i++)
|
||||||
|
{
|
||||||
|
AmprFileRegistry.Register(resolvedGuestPaths[i], resolvedHostPaths[i]);
|
||||||
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -1993,23 +2263,37 @@ public static class KernelMemoryCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int ClockGettime(CpuContext ctx)
|
public static int ClockGettime(CpuContext ctx)
|
||||||
{
|
{
|
||||||
|
var clockId = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
var timespecAddress = ctx[CpuRegister.Rsi];
|
var timespecAddress = ctx[CpuRegister.Rsi];
|
||||||
if (timespecAddress == 0)
|
if (timespecAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTimeOffset.UtcNow;
|
long seconds;
|
||||||
var seconds = now.ToUnixTimeSeconds();
|
long nanoseconds;
|
||||||
var nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
if (!KernelRuntimeCompatExports.ResolveClockTime(clockId, out seconds, out nanoseconds))
|
||||||
if (!ctx.TryWriteUInt64(timespecAddress, unchecked((ulong)seconds)) ||
|
|
||||||
!ctx.TryWriteUInt64(timespecAddress + sizeof(long), unchecked((ulong)nanoseconds)))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Einval);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
|
||||||
|
|
||||||
|
if (!ctx.Memory.TryWrite(timespecAddress, timespecBuffer))
|
||||||
|
{
|
||||||
|
KernelRuntimeCompatExports.TrySetErrno(ctx, Efault);
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)-1);
|
||||||
|
return -1;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -3846,33 +4130,59 @@ public static class KernelMemoryCompatExports
|
|||||||
|
|
||||||
private struct RegisterPrintfArgumentSource : IPrintfArgumentSource
|
private struct RegisterPrintfArgumentSource : IPrintfArgumentSource
|
||||||
{
|
{
|
||||||
|
// SysV AMD64: variadic integer/pointer args use the GP registers (rdi..r9, up to
|
||||||
|
// 6) and variadic float/double args use xmm0..xmm7 (8) — INDEPENDENT counters.
|
||||||
|
// Args beyond the registers spill to the stack in source order, so GP-overflow
|
||||||
|
// and FP-overflow share one stack cursor.
|
||||||
private readonly CpuContext _ctx;
|
private readonly CpuContext _ctx;
|
||||||
private int _gpIndex;
|
private int _gpIndex;
|
||||||
|
private int _fpIndex;
|
||||||
|
private int _stackIndex;
|
||||||
|
|
||||||
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
|
public RegisterPrintfArgumentSource(CpuContext ctx, int gpIndex)
|
||||||
{
|
{
|
||||||
_ctx = ctx;
|
_ctx = ctx;
|
||||||
_gpIndex = gpIndex;
|
_gpIndex = gpIndex;
|
||||||
|
_fpIndex = 0;
|
||||||
|
_stackIndex = 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ulong NextGpArg()
|
public ulong NextGpArg()
|
||||||
{
|
{
|
||||||
var index = _gpIndex++;
|
var index = _gpIndex;
|
||||||
return index switch
|
if (index < 6)
|
||||||
{
|
{
|
||||||
0 => _ctx[CpuRegister.Rdi],
|
_gpIndex++;
|
||||||
1 => _ctx[CpuRegister.Rsi],
|
return index switch
|
||||||
2 => _ctx[CpuRegister.Rdx],
|
{
|
||||||
3 => _ctx[CpuRegister.Rcx],
|
0 => _ctx[CpuRegister.Rdi],
|
||||||
4 => _ctx[CpuRegister.R8],
|
1 => _ctx[CpuRegister.Rsi],
|
||||||
5 => _ctx[CpuRegister.R9],
|
2 => _ctx[CpuRegister.Rdx],
|
||||||
_ => ReadStackArg(_ctx, (ulong)(index - 6) * 8)
|
3 => _ctx[CpuRegister.Rcx],
|
||||||
};
|
4 => _ctx[CpuRegister.R8],
|
||||||
|
_ => _ctx[CpuRegister.R9],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
|
||||||
}
|
}
|
||||||
|
|
||||||
public double NextFloatArg()
|
public double NextFloatArg()
|
||||||
{
|
{
|
||||||
return BitConverter.Int64BitsToDouble(unchecked((long)NextGpArg()));
|
// Float/double variadic args live in xmm0..xmm7 (low 64 bits), NOT the GP
|
||||||
|
// registers. Reading them from GP prints garbage and desynchronizes every
|
||||||
|
// subsequent argument.
|
||||||
|
ulong bits;
|
||||||
|
if (_fpIndex < 8)
|
||||||
|
{
|
||||||
|
_ctx.GetXmmRegister(_fpIndex++, out bits, out _);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
bits = ReadStackArg(_ctx, (ulong)(_stackIndex++) * 8);
|
||||||
|
}
|
||||||
|
|
||||||
|
return BitConverter.Int64BitsToDouble(unchecked((long)bits));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -4111,7 +4421,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return FileMode.Open;
|
return FileMode.Open;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static string ResolveGuestPath(string guestPath)
|
public static string ResolveGuestPath(string guestPath)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(guestPath))
|
if (string.IsNullOrWhiteSpace(guestPath))
|
||||||
{
|
{
|
||||||
@@ -4435,7 +4745,7 @@ public static class KernelMemoryCompatExports
|
|||||||
private static bool IsMutatingOpen(int flags) =>
|
private static bool IsMutatingOpen(int flags) =>
|
||||||
(flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0;
|
(flags & (O_WRONLY | O_RDWR | O_CREAT | O_TRUNC | O_APPEND)) != 0;
|
||||||
|
|
||||||
private static bool IsReadOnlyGuestMutationPath(string guestPath)
|
public static bool IsReadOnlyGuestMutationPath(string guestPath)
|
||||||
{
|
{
|
||||||
var normalized = NormalizeGuestStatCachePath(guestPath);
|
var normalized = NormalizeGuestStatCachePath(guestPath);
|
||||||
return normalized is not null &&
|
return normalized is not null &&
|
||||||
@@ -4714,7 +5024,7 @@ public static class KernelMemoryCompatExports
|
|||||||
return destination == 0 || destinationCount == 0 || TryWriteWideTerminator(ctx, destination);
|
return destination == 0 || destinationCount == 0 || TryWriteWideTerminator(ctx, destination);
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
public static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
|
||||||
{
|
{
|
||||||
value = string.Empty;
|
value = string.Empty;
|
||||||
if (address == 0 || maxLength <= 0)
|
if (address == 0 || maxLength <= 0)
|
||||||
@@ -5503,6 +5813,26 @@ public static class KernelMemoryCompatExports
|
|||||||
alignment = NormalizeLibcAlignment(alignment);
|
alignment = NormalizeLibcAlignment(alignment);
|
||||||
var actualSize = requestedSize == 0 ? 1u : requestedSize;
|
var actualSize = requestedSize == 0 ? 1u : requestedSize;
|
||||||
|
|
||||||
|
// This intentionally uses host guard pages for allocations made through the libc HLE.
|
||||||
|
// It turns an overrun that reaches the next page into an immediate access violation at
|
||||||
|
// the offending guest instruction, rather than allowing it to poison a later import.
|
||||||
|
var guardHeap = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_GUARD_HEAP"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
|
|
||||||
|
if (guardHeap && TryAllocateGuardedLibcHeap(actualSize, alignment, zeroFill, out address))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Do not silently fall back to an unguarded allocation when guard heap was explicitly
|
||||||
|
// requested: a failed setup must remain visible to the caller/reproducer.
|
||||||
|
if (guardHeap)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
nuint totalSize;
|
nuint totalSize;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
@@ -5538,7 +5868,7 @@ public static class KernelMemoryCompatExports
|
|||||||
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)IntPtr.Size, (ulong)alignment);
|
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)IntPtr.Size, (ulong)alignment);
|
||||||
lock (_libcAllocGate)
|
lock (_libcAllocGate)
|
||||||
{
|
{
|
||||||
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment);
|
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment, IsGuarded: false);
|
||||||
}
|
}
|
||||||
|
|
||||||
try
|
try
|
||||||
@@ -5558,6 +5888,51 @@ public static class KernelMemoryCompatExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static unsafe bool TryAllocateGuardedLibcHeap(nuint actualSize, nuint alignment, bool zeroFill, out ulong address)
|
||||||
|
{
|
||||||
|
address = 0;
|
||||||
|
const nuint pageSize = 0x1000;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var effectiveAlignment = Math.Max(alignment, pageSize);
|
||||||
|
var usableSize = checked((nuint)AlignUp((ulong)actualSize, (ulong)pageSize));
|
||||||
|
var reservationSize = checked(pageSize + effectiveAlignment - 1 + usableSize + pageSize);
|
||||||
|
var baseAddress = VirtualAlloc(0, reservationSize, MemCommit | MemReserve, HostPageReadWrite);
|
||||||
|
if (baseAddress == 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var alignedAddress = AlignUp(unchecked((ulong)baseAddress) + (ulong)pageSize, (ulong)effectiveAlignment);
|
||||||
|
var guardAddress = alignedAddress + (ulong)usableSize;
|
||||||
|
if (!VirtualProtect((nint)guardAddress, pageSize, HostPageNoAccess, out _))
|
||||||
|
{
|
||||||
|
_ = VirtualFree(baseAddress, 0, MemRelease);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_libcAllocGate)
|
||||||
|
{
|
||||||
|
_libcAllocations[alignedAddress] = new LibcHeapAllocation(baseAddress, actualSize, alignment, IsGuarded: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (zeroFill)
|
||||||
|
{
|
||||||
|
NativeMemory.Clear((void*)alignedAddress, actualSize);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] guard-heap alloc: ptr=0x{alignedAddress:X16} size=0x{actualSize:X} guard=0x{guardAddress:X16}");
|
||||||
|
address = alignedAddress;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static unsafe bool TryReallocateLibcHeap(ulong existingAddress, ulong requestedSize, out ulong resizedAddress)
|
private static unsafe bool TryReallocateLibcHeap(ulong existingAddress, ulong requestedSize, out ulong resizedAddress)
|
||||||
{
|
{
|
||||||
resizedAddress = 0;
|
resizedAddress = 0;
|
||||||
@@ -5666,7 +6041,14 @@ public static class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Marshal.FreeHGlobal(allocation.BaseAddress);
|
if (allocation.IsGuarded)
|
||||||
|
{
|
||||||
|
_ = VirtualFree(allocation.BaseAddress, 0, MemRelease);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
Marshal.FreeHGlobal(allocation.BaseAddress);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryMultiplyAllocationSize(ulong left, ulong right, out nuint size)
|
private static bool TryMultiplyAllocationSize(ulong left, ulong right, out nuint size)
|
||||||
@@ -5992,19 +6374,19 @@ public static class KernelMemoryCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentIndex = directory.NextIndex;
|
var currentIndex = directory.NextIndex;
|
||||||
if (basePointerAddress != 0 && !TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (currentIndex >= directory.Entries.Length)
|
if (currentIndex >= directory.Entries.Length)
|
||||||
{
|
{
|
||||||
|
if (basePointerAddress != 0 &&
|
||||||
|
!TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
var entryName = directory.Entries[currentIndex];
|
var entryName = directory.Entries[currentIndex];
|
||||||
directory.NextIndex = currentIndex + 1;
|
|
||||||
|
|
||||||
var entryBytes = Encoding.UTF8.GetBytes(entryName);
|
var entryBytes = Encoding.UTF8.GetBytes(entryName);
|
||||||
var nameLength = Math.Min(entryBytes.Length, 255);
|
var nameLength = Math.Min(entryBytes.Length, 255);
|
||||||
@@ -6023,6 +6405,13 @@ public static class KernelMemoryCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (basePointerAddress != 0 &&
|
||||||
|
!TryWriteUInt64Compat(ctx, basePointerAddress, (ulong)currentIndex))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
directory.NextIndex = currentIndex + 1;
|
||||||
ctx[CpuRegister.Rax] = 512;
|
ctx[CpuRegister.Rax] = 512;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
@@ -6272,4 +6661,15 @@ public static class KernelMemoryCompatExports
|
|||||||
sum = left + right;
|
sum = left + right;
|
||||||
return sum >= left;
|
return sum >= left;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "KMcEa+rHsIo",
|
||||||
|
ExportName = "sceKernelMapMemory",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelMapMemory(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
|
private static readonly ConcurrentDictionary<ulong, PthreadMutexState> _mutexStates = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, string> _mutexWakeKeys = new();
|
||||||
|
private static readonly ConcurrentDictionary<ulong, string> _condWakeKeys = new();
|
||||||
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
|
private static readonly Dictionary<ulong, PthreadMutexAttrState> _mutexAttrStates = new();
|
||||||
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
private static readonly Dictionary<ulong, PthreadCondState> _condStates = new();
|
||||||
private static readonly Dictionary<ulong, object> _onceGates = new();
|
private static readonly Dictionary<ulong, object> _onceGates = new();
|
||||||
@@ -35,6 +37,13 @@ public static class KernelPthreadCompatExports
|
|||||||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_CONDS"), "1", StringComparison.Ordinal);
|
||||||
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
|
private static readonly HashSet<ulong>? _tracePthreadMutexFilter = ParseTraceAddressFilter(
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREAD_MUTEX_FILTER"));
|
||||||
|
private static readonly bool _enableMutexLockBlocking =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_MUTEX_LOCK_BLOCKING"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
// On the outer class deliberately: a static on the nested state classes gives them a
|
||||||
|
// type initializer that first runs on a guest thread and fail-fasts the CLR.
|
||||||
|
private static long _nextMutexWakeId;
|
||||||
|
private static long _nextCondWakeId;
|
||||||
|
|
||||||
private sealed class PthreadMutexState
|
private sealed class PthreadMutexState
|
||||||
{
|
{
|
||||||
@@ -43,6 +52,10 @@ public static class KernelPthreadCompatExports
|
|||||||
public int RecursionCount { get; set; }
|
public int RecursionCount { get; set; }
|
||||||
public int Type { get; set; } = MutexTypeErrorCheck;
|
public int Type { get; set; } = MutexTypeErrorCheck;
|
||||||
public int Protocol { get; set; }
|
public int Protocol { get; set; }
|
||||||
|
|
||||||
|
// Keyed on state identity, not the resolved address (which can differ between a
|
||||||
|
// lock and its unlock for the same state, stranding the waiter).
|
||||||
|
public string WakeKey { get; } = "pthread_mutex#" + Interlocked.Increment(ref _nextMutexWakeId).ToString("X");
|
||||||
}
|
}
|
||||||
|
|
||||||
private sealed class PthreadMutexWaiter
|
private sealed class PthreadMutexWaiter
|
||||||
@@ -56,6 +69,23 @@ public static class KernelPthreadCompatExports
|
|||||||
public object SyncRoot { get; } = new();
|
public object SyncRoot { get; } = new();
|
||||||
public ulong SignalEpoch { get; set; }
|
public ulong SignalEpoch { get; set; }
|
||||||
public int Waiters { get; set; }
|
public int Waiters { get; set; }
|
||||||
|
|
||||||
|
// See PthreadMutexState.WakeKey.
|
||||||
|
public string WakeKey { get; } = "pthread_cond#" + Interlocked.Increment(ref _nextCondWakeId).ToString("X");
|
||||||
|
|
||||||
|
// A signal with no waiter stays pending; the guest often signals before the wait.
|
||||||
|
public int PendingSignals { get; set; }
|
||||||
|
|
||||||
|
public bool TryConsumePendingSignal()
|
||||||
|
{
|
||||||
|
if (PendingSignals <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
PendingSignals--;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
|
private readonly record struct PthreadMutexAttrState(int Type, int Protocol);
|
||||||
@@ -112,6 +142,27 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "GBUY7ywdULE",
|
||||||
|
ExportName = "scePthreadRename",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PthreadRename(CpuContext ctx)
|
||||||
|
{
|
||||||
|
if (_tracePthreads)
|
||||||
|
{
|
||||||
|
var nameAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var name = nameAddress != 0 &&
|
||||||
|
KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, nameAddress, 64, out var value)
|
||||||
|
? value
|
||||||
|
: "<unreadable>";
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] pthread.rename thread=0x{ctx[CpuRegister.Rdi]:X16} name=\"{name}\"");
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "EI-5-jlq2dE",
|
Nid = "EI-5-jlq2dE",
|
||||||
ExportName = "scePthreadGetthreadid",
|
ExportName = "scePthreadGetthreadid",
|
||||||
@@ -293,6 +344,13 @@ public static class KernelPthreadCompatExports
|
|||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
public static int PthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "27bAgiJmOh0",
|
||||||
|
ExportName = "pthread_cond_timedwait",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondTimedwait(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "kDh-NfxgMtE",
|
Nid = "kDh-NfxgMtE",
|
||||||
ExportName = "scePthreadCondSignal",
|
ExportName = "scePthreadCondSignal",
|
||||||
@@ -327,12 +385,6 @@ public static class KernelPthreadCompatExports
|
|||||||
Target = Generation.Gen4 | Generation.Gen5,
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
LibraryName = "libKernel")]
|
LibraryName = "libKernel")]
|
||||||
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
public static int PosixPthreadCondSignal(CpuContext ctx) => PthreadCondSignalCore(ctx, ctx[CpuRegister.Rdi], broadcast: false);
|
||||||
[SysAbiExport(
|
|
||||||
Nid = "27bAgiJmOh0",
|
|
||||||
ExportName = "pthread_cond_timedwait",
|
|
||||||
Target = Generation.Gen4 | Generation.Gen5,
|
|
||||||
LibraryName = "libKernel")]
|
|
||||||
public static int PosixPthreadCondTimedwait(CpuContext ctx) => PthreadCondWaitCore(ctx, ctx[CpuRegister.Rdi], ctx[CpuRegister.Rsi], timed: true, timeoutUsec: unchecked((uint)ctx[CpuRegister.Rdx]));
|
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "m5-2bsNfv7s",
|
Nid = "m5-2bsNfv7s",
|
||||||
@@ -559,42 +611,42 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Normal/adaptive mutexes do not report EDEADLK on self-lock.
|
|
||||||
// Under the current single-host-thread guest execution model,
|
|
||||||
// treating them as nested ownership keeps init paths moving
|
|
||||||
// without turning a would-block path into a hard error.
|
|
||||||
state.RecursionCount++;
|
state.RecursionCount++;
|
||||||
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, "lock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
else
|
||||||
var ownedResult = tryOnly
|
{
|
||||||
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
var ownedResult = tryOnly
|
||||||
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY
|
||||||
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||||
return ownedResult;
|
TracePthreadMutex(ctx, tryOnly ? "trylock" : "lock", mutexAddress, resolvedAddress, state, currentThreadId, ownedResult);
|
||||||
|
return ownedResult;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
var acquired = state.Semaphore.Wait(0);
|
var acquired = state.Semaphore.Wait(0);
|
||||||
if (!acquired)
|
if (!acquired)
|
||||||
{
|
{
|
||||||
// Guest-thread blocking for pthread_mutex_lock is currently disabled.
|
var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
||||||
// Demon's Souls deadlocks during PS5SyncEvent initialization.
|
// Cooperative deschedule on a contended lock corrupts fiber state
|
||||||
/* var waiter = new PthreadMutexWaiter { ThreadId = currentThreadId };
|
// (Demon's Souls sceFiberSwitch -> ESRCH). Off by default: fall through to the
|
||||||
if (!tryOnly &&
|
// synchronous host-thread wait below.
|
||||||
|
if (_enableMutexLockBlocking &&
|
||||||
|
!tryOnly &&
|
||||||
GuestThreadExecution.IsGuestThread &&
|
GuestThreadExecution.IsGuestThread &&
|
||||||
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||||
GuestThreadExecution.RequestCurrentThreadBlock(
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
ctx,
|
ctx,
|
||||||
"pthread_mutex_lock",
|
"pthread_mutex_lock",
|
||||||
GetMutexWakeKey(resolvedAddress),
|
state.WakeKey,
|
||||||
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
() => CompleteBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter),
|
||||||
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
() => TryReserveBlockedMutexLock(ctx, mutexAddress, resolvedAddress, state, waiter)))
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadMutex(ctx, "lock-block", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
} */
|
}
|
||||||
|
|
||||||
if (!tryOnly)
|
if (!tryOnly)
|
||||||
{
|
{
|
||||||
@@ -611,6 +663,7 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
|
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "lock");
|
||||||
state.OwnerThreadId = currentThreadId;
|
state.OwnerThreadId = currentThreadId;
|
||||||
state.RecursionCount = 1;
|
state.RecursionCount = 1;
|
||||||
}
|
}
|
||||||
@@ -633,16 +686,24 @@ public static class KernelPthreadCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
|
var lenientOwnership = state.Type is MutexTypeNormal or MutexTypeAdaptiveNp;
|
||||||
|
|
||||||
var shouldRelease = false;
|
var shouldRelease = false;
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
if (state.RecursionCount <= 0)
|
if (state.RecursionCount <= 0)
|
||||||
{
|
{
|
||||||
|
if (lenientOwnership)
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (requireOwner && state.OwnerThreadId != currentThreadId)
|
if (requireOwner && !lenientOwnership && state.OwnerThreadId != currentThreadId)
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED);
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
@@ -661,12 +722,15 @@ public static class KernelPthreadCompatExports
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
state.Semaphore.Release();
|
state.Semaphore.Release();
|
||||||
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetMutexWakeKey(resolvedAddress), 1);
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(state.WakeKey, 1);
|
||||||
}
|
}
|
||||||
catch (SemaphoreFullException)
|
catch (SemaphoreFullException)
|
||||||
{
|
{
|
||||||
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
if (!lenientOwnership)
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
{
|
||||||
|
TracePthreadMutex(ctx, "unlock", mutexAddress, resolvedAddress, state, currentThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1090,20 +1154,28 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
|
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
var spuriousWake = false;
|
var spuriousWake = false;
|
||||||
|
var releasedRecursion = 0;
|
||||||
lock (state.SyncRoot)
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
state.Waiters++;
|
state.Waiters++;
|
||||||
var observedEpoch = state.SignalEpoch;
|
var observedEpoch = state.SignalEpoch;
|
||||||
TracePthreadCond("wait-enter", condAddress, mutexAddress, state, timed, waitResult);
|
var consumedPendingSignal = state.TryConsumePendingSignal();
|
||||||
|
TracePthreadCond(
|
||||||
|
consumedPendingSignal ? "wait-enter-pending" : "wait-enter",
|
||||||
|
condAddress,
|
||||||
|
mutexAddress,
|
||||||
|
state,
|
||||||
|
timed,
|
||||||
|
waitResult);
|
||||||
|
|
||||||
var unlockResult = PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
var unlockResult = PthreadMutexFullUnlockForCondWait(ctx, mutexAddress, out releasedRecursion);
|
||||||
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (unlockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
state.Waiters--;
|
state.Waiters--;
|
||||||
@@ -1111,10 +1183,25 @@ public static class KernelPthreadCompatExports
|
|||||||
return unlockResult;
|
return unlockResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
var scheduler = GuestThreadExecution.Scheduler;
|
if (consumedPendingSignal)
|
||||||
if (!timed && GuestThreadExecution.RequestCurrentThreadBlock("pthread_cond_wait"))
|
|
||||||
{
|
{
|
||||||
TracePthreadCond("wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||||
|
TracePthreadCond("wait-wake-pending", condAddress, mutexAddress, state, timed, waitResult);
|
||||||
|
var pendingLockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||||
|
return pendingLockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK ? pendingLockResult : waitResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
var scheduler = GuestThreadExecution.Scheduler;
|
||||||
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
timed ? "pthread_cond_timedwait" : "pthread_cond_wait",
|
||||||
|
state.WakeKey,
|
||||||
|
() => ResumePthreadCondWait(ctx, condAddress, mutexAddress, state, observedEpoch, timed, releasedRecursion),
|
||||||
|
() => state.SignalEpoch != observedEpoch,
|
||||||
|
timed ? GuestThreadExecution.ComputeDeadlineTimestamp(GetCondWaitTimeout(timeoutUsec)) : 0))
|
||||||
|
{
|
||||||
|
TracePthreadCond(timed ? "wait-block-timed" : "wait-block", condAddress, mutexAddress, state, timed, waitResult);
|
||||||
return waitResult;
|
return waitResult;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1137,6 +1224,21 @@ public static class KernelPthreadCompatExports
|
|||||||
{
|
{
|
||||||
if (!Monitor.Wait(state.SyncRoot, GetCondSpuriousWakeTimeout()))
|
if (!Monitor.Wait(state.SyncRoot, GetCondSpuriousWakeTimeout()))
|
||||||
{
|
{
|
||||||
|
if (scheduler is not null && !GuestThreadExecution.IsGuestThread)
|
||||||
|
{
|
||||||
|
Monitor.Exit(state.SyncRoot);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
scheduler.Pump(ctx, "pthread_cond_wait");
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Monitor.Enter(state.SyncRoot);
|
||||||
|
}
|
||||||
|
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
spuriousWake = true;
|
spuriousWake = true;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
@@ -1163,7 +1265,7 @@ public static class KernelPthreadCompatExports
|
|||||||
waitResult);
|
waitResult);
|
||||||
}
|
}
|
||||||
|
|
||||||
var lockResult = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
|
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||||
if (lockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
if (lockResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
TracePthreadCond("wait-relock-fail", condAddress, mutexAddress, state, timed, lockResult);
|
TracePthreadCond("wait-relock-fail", condAddress, mutexAddress, state, timed, lockResult);
|
||||||
@@ -1189,16 +1291,19 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out _, out var state))
|
if (!TryResolveCondState(ctx, condAddress, createIfZero: true, out var resolvedCondAddress, out var state))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var shouldWakeScheduler = false;
|
||||||
lock (state.SyncRoot)
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
|
// Advance the epoch even with no waiter registered — it's the wait predicate.
|
||||||
|
state.SignalEpoch++;
|
||||||
if (state.Waiters > 0)
|
if (state.Waiters > 0)
|
||||||
{
|
{
|
||||||
state.SignalEpoch++;
|
shouldWakeScheduler = true;
|
||||||
if (broadcast)
|
if (broadcast)
|
||||||
{
|
{
|
||||||
Monitor.PulseAll(state.SyncRoot);
|
Monitor.PulseAll(state.SyncRoot);
|
||||||
@@ -1208,15 +1313,135 @@ public static class KernelPthreadCompatExports
|
|||||||
Monitor.Pulse(state.SyncRoot);
|
Monitor.Pulse(state.SyncRoot);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
state.PendingSignals++;
|
||||||
|
}
|
||||||
|
|
||||||
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
TracePthreadCond(broadcast ? "broadcast" : "signal", condAddress, mutexAddress: 0, state, timed: false, (int)OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (shouldWakeScheduler)
|
||||||
|
{
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(
|
||||||
|
state.WakeKey,
|
||||||
|
broadcast ? int.MaxValue : 1);
|
||||||
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static int ResumePthreadCondWait(
|
||||||
|
CpuContext ctx,
|
||||||
|
ulong condAddress,
|
||||||
|
ulong mutexAddress,
|
||||||
|
PthreadCondState state,
|
||||||
|
ulong observedEpoch,
|
||||||
|
bool timed,
|
||||||
|
int releasedRecursion)
|
||||||
|
{
|
||||||
|
var waitResult = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
lock (state.SyncRoot)
|
||||||
|
{
|
||||||
|
state.Waiters = Math.Max(0, state.Waiters - 1);
|
||||||
|
if (timed && state.SignalEpoch == observedEpoch)
|
||||||
|
{
|
||||||
|
waitResult = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
|
||||||
|
}
|
||||||
|
|
||||||
|
TracePthreadCond(
|
||||||
|
waitResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? "wait-resume" : "wait-resume-timeout",
|
||||||
|
condAddress,
|
||||||
|
mutexAddress,
|
||||||
|
state,
|
||||||
|
timed,
|
||||||
|
waitResult);
|
||||||
|
}
|
||||||
|
|
||||||
|
var lockResult = PthreadMutexRelockForCondWait(ctx, mutexAddress, releasedRecursion);
|
||||||
|
return lockResult == (int)OrbisGen2Result.ORBIS_GEN2_OK ? waitResult : lockResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetCondWakeKey(ulong resolvedCondAddress) =>
|
||||||
|
_condWakeKeys.GetOrAdd(resolvedCondAddress, static address => string.Create(
|
||||||
|
31,
|
||||||
|
address,
|
||||||
|
static (destination, value) =>
|
||||||
|
{
|
||||||
|
"pthread_cond:0x".AsSpan().CopyTo(destination);
|
||||||
|
_ = value.TryFormat(destination[15..], out _, "X16");
|
||||||
|
}));
|
||||||
|
|
||||||
|
private static int PthreadMutexFullUnlockForCondWait(CpuContext ctx, ulong mutexAddress, out int releasedRecursion)
|
||||||
|
{
|
||||||
|
releasedRecursion = 0;
|
||||||
|
if (!TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
|
||||||
|
{
|
||||||
|
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
var currentThreadId = KernelPthreadState.GetCurrentThreadHandle();
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
if (state.RecursionCount <= 0 || state.OwnerThreadId != currentThreadId)
|
||||||
|
{
|
||||||
|
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
releasedRecursion = state.RecursionCount;
|
||||||
|
state.RecursionCount = 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
return PthreadMutexUnlockCore(ctx, mutexAddress, requireOwner: true);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int PthreadMutexRelockForCondWait(CpuContext ctx, ulong mutexAddress, int releasedRecursion)
|
||||||
|
{
|
||||||
|
var result = PthreadMutexLockCore(ctx, mutexAddress, tryOnly: false);
|
||||||
|
if (result != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (releasedRecursion > 1 &&
|
||||||
|
TryResolveMutexState(ctx, mutexAddress, createIfZero: false, out _, out var state))
|
||||||
|
{
|
||||||
|
lock (state)
|
||||||
|
{
|
||||||
|
if (state.OwnerThreadId == KernelPthreadState.GetCurrentThreadHandle())
|
||||||
|
{
|
||||||
|
state.RecursionCount = releasedRecursion;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static string? DumpMutexStateForStall(ulong mutexAddress)
|
||||||
|
{
|
||||||
|
if (!_mutexStates.TryGetValue(mutexAddress, out var state))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
var waiterThreads = string.Join(",", _mutexStates
|
||||||
|
.Where(pair => ReferenceEquals(pair.Value, state))
|
||||||
|
.Select(pair => $"0x{pair.Key:X}"));
|
||||||
|
|
||||||
|
return $"mutex=0x{mutexAddress:X16} owner=0x{state.OwnerThreadId:X} recursion={state.RecursionCount} " +
|
||||||
|
$"type={state.Type} semaphore_count={state.Semaphore.CurrentCount} aliases=[{waiterThreads}]";
|
||||||
|
}
|
||||||
|
|
||||||
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
|
private static string GetMutexWakeKey(ulong resolvedMutexAddress) =>
|
||||||
$"pthread_mutex:0x{resolvedMutexAddress:X16}";
|
_mutexWakeKeys.GetOrAdd(resolvedMutexAddress, static address => string.Create(
|
||||||
|
32,
|
||||||
|
address,
|
||||||
|
static (destination, value) =>
|
||||||
|
{
|
||||||
|
"pthread_mutex:0x".AsSpan().CopyTo(destination);
|
||||||
|
_ = value.TryFormat(destination[16..], out _, "X16");
|
||||||
|
}));
|
||||||
|
|
||||||
private static bool TryReserveBlockedMutexLock(
|
private static bool TryReserveBlockedMutexLock(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
@@ -1233,6 +1458,13 @@ public static class KernelPthreadCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!state.Semaphore.Wait(0))
|
||||||
|
{
|
||||||
|
TracePthreadMutex(ctx, "lock-reserve-busy", mutexAddress, resolvedAddress, state, waiter.ThreadId, (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectMutexDoubleOwner(resolvedAddress, state, waiter.ThreadId, "reserve");
|
||||||
state.OwnerThreadId = waiter.ThreadId;
|
state.OwnerThreadId = waiter.ThreadId;
|
||||||
state.RecursionCount = 1;
|
state.RecursionCount = 1;
|
||||||
Interlocked.Exchange(ref waiter.Reserved, 1);
|
Interlocked.Exchange(ref waiter.Reserved, 1);
|
||||||
@@ -1264,6 +1496,7 @@ public static class KernelPthreadCompatExports
|
|||||||
|
|
||||||
lock (state)
|
lock (state)
|
||||||
{
|
{
|
||||||
|
DetectMutexDoubleOwner(resolvedAddress, state, currentThreadId, "resume");
|
||||||
state.OwnerThreadId = currentThreadId;
|
state.OwnerThreadId = currentThreadId;
|
||||||
state.RecursionCount = 1;
|
state.RecursionCount = 1;
|
||||||
}
|
}
|
||||||
@@ -1272,6 +1505,19 @@ public static class KernelPthreadCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A nonzero foreign prior owner means two guest threads are in the same critical
|
||||||
|
// section. Call while holding lock(state).
|
||||||
|
private static void DetectMutexDoubleOwner(ulong resolvedAddress, PthreadMutexState state, ulong currentThreadId, string site)
|
||||||
|
{
|
||||||
|
var priorOwner = state.OwnerThreadId;
|
||||||
|
if (priorOwner != 0 && priorOwner != currentThreadId)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] MUTEX DOUBLE-OWNER at {site}: resolved=0x{resolvedAddress:X} acquirer=0x{currentThreadId:X} " +
|
||||||
|
$"prior_owner=0x{priorOwner:X} recursion={state.RecursionCount} type={state.Type} sem_count={state.Semaphore.CurrentCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
|
private static TimeSpan GetCondWaitTimeout(uint timeoutUsec)
|
||||||
{
|
{
|
||||||
if (timeoutUsec == 0)
|
if (timeoutUsec == 0)
|
||||||
@@ -1409,7 +1655,8 @@ public static class KernelPthreadCompatExports
|
|||||||
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
$"[LOADER][TRACE] pthread_{operation}: mutex=0x{mutexAddress:X16} resolved=0x{resolvedAddress:X16} " +
|
||||||
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
|
$"guest[0]=0x{guestWord0:X16} guest[8]=0x{guestWord1:X16} " +
|
||||||
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
|
$"current=0x{currentThreadId:X16} owner=0x{(state?.OwnerThreadId ?? 0):X16} " +
|
||||||
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8}");
|
$"recursion={(state?.RecursionCount ?? 0)} type={(state?.Type ?? 0)} result=0x{unchecked((uint)result):X8} " +
|
||||||
|
$"guest_thread=0x{GuestThreadExecution.CurrentGuestThreadHandle:X16} managed={Environment.CurrentManagedThreadId}");
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
|
private static void TracePthreadCond(string operation, ulong condAddress, ulong mutexAddress, PthreadCondState? state, bool timed, int result)
|
||||||
|
|||||||
@@ -31,6 +31,8 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
private static long _nextSyntheticRwlockHandleId = 1;
|
private static long _nextSyntheticRwlockHandleId = 1;
|
||||||
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
private static long _nextSyntheticPthreadAttrHandleId = 1;
|
||||||
private static long _nextSyntheticRwlockAttrHandleId = 1;
|
private static long _nextSyntheticRwlockAttrHandleId = 1;
|
||||||
|
private static readonly bool _strictRwlockWriterPreference =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_STRICT_RWLOCK_WRITER_PREFERENCE"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
private static readonly ConcurrentDictionary<ulong, ConcurrentDictionary<int, ulong>> _threadLocalSpecific = new();
|
||||||
|
|
||||||
@@ -85,14 +87,23 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
|
public PthreadAttrState Attributes { get; set; } = PthreadAttrState.Default;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// On the outer class deliberately: a static on the nested state class gives it a type
|
||||||
|
// initializer that first runs on a guest thread and fail-fasts the CLR.
|
||||||
|
private static long _nextRwlockWakeId;
|
||||||
|
|
||||||
private sealed class PthreadRwlockState
|
private sealed class PthreadRwlockState
|
||||||
{
|
{
|
||||||
public object SyncRoot { get; } = new();
|
public object SyncRoot { get; } = new();
|
||||||
public Dictionary<ulong, int> ReaderCounts { get; } = new();
|
public Dictionary<ulong, int> ReaderCounts { get; } = new();
|
||||||
|
public Dictionary<ulong, int> CompatWriterCounts { get; } = new();
|
||||||
public int ReaderTotalCount { get; set; }
|
public int ReaderTotalCount { get; set; }
|
||||||
|
public int CompatWriterTotalCount { get; set; }
|
||||||
public ulong WriterThreadId { get; set; }
|
public ulong WriterThreadId { get; set; }
|
||||||
public int WaitingWriters { get; set; }
|
public int WaitingWriters { get; set; }
|
||||||
|
|
||||||
|
// See PthreadMutexState.WakeKey.
|
||||||
|
public string WakeKey { get; } = "pthread_rwlock#" + Interlocked.Increment(ref _nextRwlockWakeId).ToString("X");
|
||||||
|
|
||||||
public int GetReaderCount(ulong threadId)
|
public int GetReaderCount(ulong threadId)
|
||||||
{
|
{
|
||||||
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
|
return ReaderCounts.TryGetValue(threadId, out var count) ? count : 0;
|
||||||
@@ -105,6 +116,33 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
ReaderTotalCount++;
|
ReaderTotalCount++;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void AddCompatWriter(ulong threadId)
|
||||||
|
{
|
||||||
|
CompatWriterCounts.TryGetValue(threadId, out var currentCount);
|
||||||
|
CompatWriterCounts[threadId] = currentCount + 1;
|
||||||
|
CompatWriterTotalCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public bool RemoveCompatWriter(ulong threadId)
|
||||||
|
{
|
||||||
|
if (!CompatWriterCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentCount == 1)
|
||||||
|
{
|
||||||
|
CompatWriterCounts.Remove(threadId);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
CompatWriterCounts[threadId] = currentCount - 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
CompatWriterTotalCount = Math.Max(0, CompatWriterTotalCount - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
public bool RemoveReader(ulong threadId)
|
public bool RemoveReader(ulong threadId)
|
||||||
{
|
{
|
||||||
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
if (!ReaderCounts.TryGetValue(threadId, out var currentCount) || currentCount <= 0)
|
||||||
@@ -935,7 +973,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
|
|
||||||
lock (state.SyncRoot)
|
lock (state.SyncRoot)
|
||||||
{
|
{
|
||||||
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0)
|
if (state.WriterThreadId != 0 || state.ReaderTotalCount != 0 || state.WaitingWriters != 0 || state.CompatWriterTotalCount != 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
|
||||||
}
|
}
|
||||||
@@ -1003,7 +1041,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out _, out var rwlock))
|
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: false, out var resolvedAddress, out var rwlock))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -1014,7 +1052,11 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
{
|
{
|
||||||
lock (rwlock.SyncRoot)
|
lock (rwlock.SyncRoot)
|
||||||
{
|
{
|
||||||
if (rwlock.WriterThreadId == currentThreadId)
|
if (rwlock.RemoveCompatWriter(currentThreadId))
|
||||||
|
{
|
||||||
|
Monitor.PulseAll(rwlock.SyncRoot);
|
||||||
|
}
|
||||||
|
else if (rwlock.WriterThreadId == currentThreadId)
|
||||||
{
|
{
|
||||||
rwlock.WriterThreadId = 0;
|
rwlock.WriterThreadId = 0;
|
||||||
Monitor.PulseAll(rwlock.SyncRoot);
|
Monitor.PulseAll(rwlock.SyncRoot);
|
||||||
@@ -1037,6 +1079,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(rwlock.WakeKey);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1227,7 +1270,7 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out _, out var rwlock))
|
if (!TryResolveRwlockState(ctx, rwlockAddress, createIfZero: true, out var resolvedAddress, out var rwlock))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
}
|
}
|
||||||
@@ -1242,20 +1285,60 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId) > 0)
|
||||||
|
{
|
||||||
|
rwlock.AddCompatWriter(currentThreadId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
!_strictRwlockWriterPreference &&
|
||||||
|
rwlock.WriterThreadId == 0 &&
|
||||||
|
rwlock.ReaderTotalCount == 0 &&
|
||||||
|
rwlock.CompatWriterTotalCount == 0)
|
||||||
|
{
|
||||||
|
rwlock.AddCompatWriter(currentThreadId);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rwlock.WriterThreadId == 0 && rwlock.ReaderTotalCount == 0 && rwlock.CompatWriterTotalCount == 0)
|
||||||
|
{
|
||||||
|
DetectRwlockWriterConflict(resolvedAddress, rwlock, currentThreadId, "wrlock");
|
||||||
|
rwlock.WriterThreadId = currentThreadId;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
rwlock.WaitingWriters++;
|
rwlock.WaitingWriters++;
|
||||||
|
var transferredToScheduler = false;
|
||||||
try
|
try
|
||||||
{
|
{
|
||||||
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0)
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"pthread_rwlock_wrlock",
|
||||||
|
rwlock.WakeKey,
|
||||||
|
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: true)))
|
||||||
|
{
|
||||||
|
transferredToScheduler = true;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
while (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||||
{
|
{
|
||||||
Monitor.Wait(rwlock.SyncRoot);
|
Monitor.Wait(rwlock.SyncRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rwlock.WriterThreadId = currentThreadId;
|
||||||
}
|
}
|
||||||
finally
|
finally
|
||||||
{
|
{
|
||||||
rwlock.WaitingWriters--;
|
if (!transferredToScheduler)
|
||||||
|
{
|
||||||
|
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
rwlock.WriterThreadId = currentThreadId;
|
|
||||||
}
|
}
|
||||||
else
|
else
|
||||||
{
|
{
|
||||||
@@ -1264,12 +1347,30 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
|
||||||
}
|
}
|
||||||
|
|
||||||
while (rwlock.WriterThreadId != 0 ||
|
while (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||||
(rwlock.WaitingWriters > 0 && rwlock.GetReaderCount(currentThreadId) == 0))
|
|
||||||
{
|
{
|
||||||
|
if (GuestThreadExecution.IsGuestThread &&
|
||||||
|
GuestThreadExecution.TryGetCurrentImportCallFrame(out _) &&
|
||||||
|
GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"pthread_rwlock_rdlock",
|
||||||
|
rwlock.WakeKey,
|
||||||
|
static () => (int)OrbisGen2Result.ORBIS_GEN2_OK,
|
||||||
|
() => TryAcquireBlockedRwlock(rwlock, currentThreadId, write: false)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
Monitor.Wait(rwlock.SyncRoot);
|
Monitor.Wait(rwlock.SyncRoot);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (rwlock.WriterThreadId != 0 ||
|
||||||
|
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] RWLOCK READER/WRITER COEXIST: resolved=0x{resolvedAddress:X} reader=0x{currentThreadId:X} " +
|
||||||
|
$"writer=0x{rwlock.WriterThreadId:X} compat_total={rwlock.CompatWriterTotalCount} readers_total={rwlock.ReaderTotalCount}");
|
||||||
|
}
|
||||||
rwlock.AddReader(currentThreadId);
|
rwlock.AddReader(currentThreadId);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1277,6 +1378,86 @@ public static class KernelPthreadExtendedCompatExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static bool TryAcquireBlockedRwlock(PthreadRwlockState rwlock, ulong currentThreadId, bool write)
|
||||||
|
{
|
||||||
|
lock (rwlock.SyncRoot)
|
||||||
|
{
|
||||||
|
if (write)
|
||||||
|
{
|
||||||
|
if (rwlock.WriterThreadId != 0 || rwlock.ReaderTotalCount != 0 || rwlock.CompatWriterTotalCount != 0)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
DetectRwlockWriterConflict(0, rwlock, currentThreadId, "wrlock-resume");
|
||||||
|
rwlock.WriterThreadId = currentThreadId;
|
||||||
|
rwlock.WaitingWriters = Math.Max(0, rwlock.WaitingWriters - 1);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (ReaderMustWaitForRwlock(rwlock, currentThreadId))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
rwlock.AddReader(currentThreadId);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Call while holding lock(rwlock.SyncRoot): an existing reader/writer here means a
|
||||||
|
// writer would share the rwlock with another holder — a data race.
|
||||||
|
private static void DetectRwlockWriterConflict(ulong resolvedAddress, PthreadRwlockState rwlock, ulong currentThreadId, string site)
|
||||||
|
{
|
||||||
|
if (rwlock.WriterThreadId != 0 ||
|
||||||
|
rwlock.ReaderTotalCount != 0 ||
|
||||||
|
rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][ERROR] RWLOCK WRITER CONFLICT at {site}: resolved=0x{resolvedAddress:X} writer=0x{currentThreadId:X} " +
|
||||||
|
$"existing_writer=0x{rwlock.WriterThreadId:X} readers_total={rwlock.ReaderTotalCount} compat_total={rwlock.CompatWriterTotalCount}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool ReaderMustWaitForRwlock(PthreadRwlockState rwlock, ulong currentThreadId)
|
||||||
|
{
|
||||||
|
if (rwlock.WriterThreadId != 0)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (rwlock.CompatWriterTotalCount > rwlock.CompatWriterCounts.GetValueOrDefault(currentThreadId))
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return rwlock.WaitingWriters > 0 &&
|
||||||
|
rwlock.GetReaderCount(currentThreadId) == 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetRwlockWakeKey(ulong rwlockAddress) => $"pthread_rwlock:0x{rwlockAddress:X16}";
|
||||||
|
|
||||||
|
public static string? DumpRwlockStateForStall(ulong rwlockAddress)
|
||||||
|
{
|
||||||
|
PthreadRwlockState? rwlock;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (!_rwlockStates.TryGetValue(rwlockAddress, out rwlock))
|
||||||
|
{
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (rwlock.SyncRoot)
|
||||||
|
{
|
||||||
|
var readers = string.Join(",", rwlock.ReaderCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
|
||||||
|
var compatWriters = string.Join(",", rwlock.CompatWriterCounts.Select(pair => $"0x{pair.Key:X}x{pair.Value}"));
|
||||||
|
return $"rwlock=0x{rwlockAddress:X16} writer=0x{rwlock.WriterThreadId:X} waiting_writers={rwlock.WaitingWriters} " +
|
||||||
|
$"readers_total={rwlock.ReaderTotalCount} readers=[{readers}] " +
|
||||||
|
$"compat_writers_total={rwlock.CompatWriterTotalCount} compat_writers=[{compatWriters}]";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static ulong ResolveRwlockHandle(CpuContext ctx, ulong rwlockAddress)
|
private static ulong ResolveRwlockHandle(CpuContext ctx, ulong rwlockAddress)
|
||||||
{
|
{
|
||||||
if (rwlockAddress == 0)
|
if (rwlockAddress == 0)
|
||||||
|
|||||||
@@ -14,6 +14,22 @@ namespace SharpEmu.Libs.Kernel;
|
|||||||
|
|
||||||
public static class KernelRuntimeCompatExports
|
public static class KernelRuntimeCompatExports
|
||||||
{
|
{
|
||||||
|
internal const int ClockRealtime = 0;
|
||||||
|
internal const int ClockVirtual = 1;
|
||||||
|
internal const int ClockProf = 2;
|
||||||
|
internal const int ClockMonotonic = 4;
|
||||||
|
internal const int ClockUptime = 5;
|
||||||
|
internal const int ClockUptimePrecise = 7;
|
||||||
|
internal const int ClockUptimeFast = 8;
|
||||||
|
internal const int ClockRealtimePrecise = 9;
|
||||||
|
internal const int ClockRealtimeFast = 10;
|
||||||
|
internal const int ClockMonotonicPrecise = 11;
|
||||||
|
internal const int ClockMonotonicFast = 12;
|
||||||
|
internal const int ClockSecond = 13;
|
||||||
|
internal const int ClockThreadCputimeId = 14;
|
||||||
|
internal const int ClockProcTime = 15;
|
||||||
|
private const int Efault = 14;
|
||||||
|
private const int Einval = 22;
|
||||||
private const ulong TlsErrnoOffset = 0x40;
|
private const ulong TlsErrnoOffset = 0x40;
|
||||||
private const ulong TlsStackChkGuardBaseOffset = 0x800;
|
private const ulong TlsStackChkGuardBaseOffset = 0x800;
|
||||||
private const ulong StackChkGuardFieldOffset = 0x10;
|
private const ulong StackChkGuardFieldOffset = 0x10;
|
||||||
@@ -64,6 +80,77 @@ public static class KernelRuntimeCompatExports
|
|||||||
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
|
||||||
private delegate ulong RdtscDelegate();
|
private delegate ulong RdtscDelegate();
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "QvsZxomvUHs",
|
||||||
|
ExportName = "sceKernelNanosleep",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libKernel")]
|
||||||
|
public static int KernelNanosleep(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var requestAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var remainAddress = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
if (requestAddress == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = Einval;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||||
|
if (!ctx.Memory.TryRead(requestAddress, timespecBuffer))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = Efault;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var tvSec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer);
|
||||||
|
var tvNsec = BinaryPrimitives.ReadInt64LittleEndian(timespecBuffer[sizeof(long)..]);
|
||||||
|
|
||||||
|
if (tvSec < 0 || tvNsec < 0 || tvNsec >= 1_000_000_000L)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = Einval;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (tvSec == 0 && tvNsec == 0)
|
||||||
|
{
|
||||||
|
WriteRemainingTime(ctx, remainAddress, 0, 0);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
GuestThreadExecution.Scheduler?.Pump(ctx, "sceKernelNanosleep");
|
||||||
|
|
||||||
|
// TimeSpan resolution is 100 ns ticks, so sub-100 ns requests round up to
|
||||||
|
// a single tick rather than collapsing to a zero-length (no-op) sleep.
|
||||||
|
var totalTicks = tvSec * TimeSpan.TicksPerSecond + Math.Max(tvNsec / 100L, 1L);
|
||||||
|
try
|
||||||
|
{
|
||||||
|
Thread.Sleep(TimeSpan.FromTicks(totalTicks));
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
Thread.Sleep(TimeSpan.FromMilliseconds(int.MaxValue));
|
||||||
|
}
|
||||||
|
|
||||||
|
WriteRemainingTime(ctx, remainAddress, 0, 0);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void WriteRemainingTime(CpuContext ctx, ulong remainAddress, long seconds, long nanoseconds)
|
||||||
|
{
|
||||||
|
if (remainAddress == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> remainBuffer = stackalloc byte[16];
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(remainBuffer, seconds);
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(remainBuffer[sizeof(long)..], nanoseconds);
|
||||||
|
ctx.Memory.TryWrite(remainAddress, remainBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "1jfXLRVzisc",
|
Nid = "1jfXLRVzisc",
|
||||||
ExportName = "sceKernelUsleep",
|
ExportName = "sceKernelUsleep",
|
||||||
@@ -186,21 +273,16 @@ public static class KernelRuntimeCompatExports
|
|||||||
|
|
||||||
long seconds;
|
long seconds;
|
||||||
long nanoseconds;
|
long nanoseconds;
|
||||||
if (clockId == 0)
|
if (!ResolveClockTime(clockId, out seconds, out nanoseconds))
|
||||||
{
|
{
|
||||||
var now = DateTimeOffset.UtcNow;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
seconds = now.ToUnixTimeSeconds();
|
|
||||||
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
|
||||||
}
|
|
||||||
else
|
|
||||||
{
|
|
||||||
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
|
|
||||||
seconds = elapsedTicks / Stopwatch.Frequency;
|
|
||||||
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
|
Span<byte> timespecBuffer = stackalloc byte[16];
|
||||||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)nanoseconds)))
|
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer, seconds);
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(timespecBuffer[sizeof(long)..], nanoseconds);
|
||||||
|
|
||||||
|
if (!ctx.Memory.TryWrite(timeAddress, timespecBuffer))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -225,8 +307,11 @@ public static class KernelRuntimeCompatExports
|
|||||||
var now = DateTimeOffset.UtcNow;
|
var now = DateTimeOffset.UtcNow;
|
||||||
var seconds = now.ToUnixTimeSeconds();
|
var seconds = now.ToUnixTimeSeconds();
|
||||||
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
|
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
|
||||||
if (!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
|
|
||||||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds)))
|
Span<byte> timevalBuffer = stackalloc byte[16];
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer, seconds);
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer[sizeof(long)..], microseconds);
|
||||||
|
if (!ctx.Memory.TryWrite(timeAddress, timevalBuffer))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
@@ -248,18 +333,28 @@ public static class KernelRuntimeCompatExports
|
|||||||
var seconds = now.ToUnixTimeSeconds();
|
var seconds = now.ToUnixTimeSeconds();
|
||||||
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
|
var microseconds = (now.Ticks % TimeSpan.TicksPerSecond) / 10;
|
||||||
|
|
||||||
if (timeAddress != 0 &&
|
if (timeAddress != 0)
|
||||||
(!ctx.TryWriteUInt64(timeAddress, unchecked((ulong)seconds)) ||
|
|
||||||
!ctx.TryWriteUInt64(timeAddress + sizeof(long), unchecked((ulong)microseconds))))
|
|
||||||
{
|
{
|
||||||
return -1;
|
Span<byte> timevalBuffer = stackalloc byte[16];
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer, seconds);
|
||||||
|
BinaryPrimitives.WriteInt64LittleEndian(timevalBuffer[sizeof(long)..], microseconds);
|
||||||
|
if (!ctx.Memory.TryWrite(timeAddress, timevalBuffer))
|
||||||
|
{
|
||||||
|
TrySetErrno(ctx, Efault);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (timezoneAddress != 0 &&
|
if (timezoneAddress != 0)
|
||||||
(!ctx.TryWriteInt32(timezoneAddress, 0) ||
|
|
||||||
!ctx.TryWriteInt32(timezoneAddress + sizeof(int), 0)))
|
|
||||||
{
|
{
|
||||||
return -1;
|
Span<byte> timezoneBuffer = stackalloc byte[8];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(timezoneBuffer, 0);
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(timezoneBuffer[sizeof(int)..], 0);
|
||||||
|
if (!ctx.Memory.TryWrite(timezoneAddress, timezoneBuffer))
|
||||||
|
{
|
||||||
|
TrySetErrno(ctx, Efault);
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
@@ -617,6 +712,57 @@ public static class KernelRuntimeCompatExports
|
|||||||
return address != 0 && ctx.TryWriteInt32(address, value);
|
return address != 0 && ctx.TryWriteInt32(address, value);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
internal static bool ResolveClockTime(int clockId, out long seconds, out long nanoseconds)
|
||||||
|
{
|
||||||
|
switch (clockId)
|
||||||
|
{
|
||||||
|
case ClockRealtime:
|
||||||
|
case ClockRealtimePrecise:
|
||||||
|
case ClockRealtimeFast:
|
||||||
|
case ClockVirtual:
|
||||||
|
case ClockProf:
|
||||||
|
{
|
||||||
|
var now = DateTimeOffset.UtcNow;
|
||||||
|
seconds = now.ToUnixTimeSeconds();
|
||||||
|
nanoseconds = (now.Ticks % TimeSpan.TicksPerSecond) * 100;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// CLOCK_SECOND is FreeBSD's cached whole-second realtime clock (Quake's
|
||||||
|
// audio_output_thread polls it and treated the previous EINVAL as a fatal
|
||||||
|
// init failure, exiting and getting respawned in a loop).
|
||||||
|
case ClockSecond:
|
||||||
|
seconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
|
||||||
|
nanoseconds = 0;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case ClockMonotonic:
|
||||||
|
case ClockMonotonicPrecise:
|
||||||
|
case ClockMonotonicFast:
|
||||||
|
case ClockUptime:
|
||||||
|
case ClockUptimePrecise:
|
||||||
|
case ClockUptimeFast:
|
||||||
|
// Per-thread/process CPU time approximated with the monotonic clock; games
|
||||||
|
// use these for profiling deltas where monotonicity matters, not absolutes.
|
||||||
|
case ClockThreadCputimeId:
|
||||||
|
case ClockProcTime:
|
||||||
|
GetProcessMonotonicTime(out seconds, out nanoseconds);
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
seconds = 0;
|
||||||
|
nanoseconds = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static void GetProcessMonotonicTime(out long seconds, out long nanoseconds)
|
||||||
|
{
|
||||||
|
var elapsedTicks = Stopwatch.GetTimestamp() - _processStartCounter;
|
||||||
|
seconds = elapsedTicks / Stopwatch.Frequency;
|
||||||
|
nanoseconds = (elapsedTicks % Stopwatch.Frequency) * 1_000_000_000L / Stopwatch.Frequency;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "bnZxYgAFeA0",
|
Nid = "bnZxYgAFeA0",
|
||||||
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
|
ExportName = "sceKernelGetSanitizerNewReplaceExternal",
|
||||||
@@ -1469,26 +1615,42 @@ public static class KernelRuntimeCompatExports
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
var bytes = new List<byte>(Math.Min(maxLength, 64));
|
const int pageSize = 4096;
|
||||||
Span<byte> one = stackalloc byte[1];
|
const int inlineChunkSize = 64;
|
||||||
for (var i = 0; i < maxLength; i++)
|
Span<byte> buffer = stackalloc byte[Math.Min(maxLength, 512)];
|
||||||
|
var length = 0;
|
||||||
|
|
||||||
|
for (var offset = 0; offset < maxLength && length < buffer.Length;)
|
||||||
{
|
{
|
||||||
if (!ctx.Memory.TryRead(address + (ulong)i, one))
|
var current = address + (ulong)offset;
|
||||||
|
var pageRemaining = pageSize - (int)(current & (pageSize - 1));
|
||||||
|
var chunkSize = Math.Min(
|
||||||
|
buffer.Length - length,
|
||||||
|
Math.Min(maxLength - offset, Math.Min(inlineChunkSize, pageRemaining)));
|
||||||
|
|
||||||
|
if (chunkSize <= 0)
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (one[0] == 0)
|
var chunk = buffer.Slice(length, chunkSize);
|
||||||
|
if (!ctx.Memory.TryRead(current, chunk))
|
||||||
{
|
{
|
||||||
value = Encoding.UTF8.GetString(bytes.ToArray());
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var nulIndex = chunk.IndexOf((byte)0);
|
||||||
|
if (nulIndex >= 0)
|
||||||
|
{
|
||||||
|
value = Encoding.UTF8.GetString(buffer[..(length + nulIndex)]);
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
bytes.Add(one[0]);
|
length += chunkSize;
|
||||||
|
offset += chunkSize;
|
||||||
}
|
}
|
||||||
|
|
||||||
value = Encoding.UTF8.GetString(bytes.ToArray());
|
return false;
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
private static ulong GetTlsScratchAddress(CpuContext ctx, ulong offset)
|
||||||
@@ -1697,7 +1859,7 @@ public static class KernelRuntimeCompatExports
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
Span<byte> stub = stackalloc byte[]
|
ReadOnlySpan<byte> stub = stackalloc byte[]
|
||||||
{
|
{
|
||||||
0x0F, 0x31,
|
0x0F, 0x31,
|
||||||
0x48, 0xC1, 0xE2, 0x20,
|
0x48, 0xC1, 0xE2, 0x20,
|
||||||
@@ -1705,7 +1867,14 @@ public static class KernelRuntimeCompatExports
|
|||||||
0xC3,
|
0xC3,
|
||||||
};
|
};
|
||||||
|
|
||||||
Marshal.Copy(stub.ToArray(), 0, stubAddress, stub.Length);
|
unsafe
|
||||||
|
{
|
||||||
|
fixed (byte* src = stub)
|
||||||
|
{
|
||||||
|
Buffer.MemoryCopy(src, (void*)stubAddress, stub.Length, stub.Length);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress);
|
return Marshal.GetDelegateForFunctionPointer<RdtscDelegate>(stubAddress);
|
||||||
}
|
}
|
||||||
catch
|
catch
|
||||||
|
|||||||
@@ -19,9 +19,22 @@ public static class KernelSemaphoreCompatExports
|
|||||||
public required int MaxCount { get; init; }
|
public required int MaxCount { get; init; }
|
||||||
public int Count { get; set; }
|
public int Count { get; set; }
|
||||||
public int WaitingThreads { get; set; }
|
public int WaitingThreads { get; set; }
|
||||||
|
public int CancelEpoch { get; set; }
|
||||||
|
public bool Deleted { get; set; }
|
||||||
public object Gate { get; } = new();
|
public object Gate { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private sealed class SemaphoreWaiter
|
||||||
|
{
|
||||||
|
public required int NeedCount { get; init; }
|
||||||
|
public required int CancelEpochAtBlock { get; init; }
|
||||||
|
|
||||||
|
// Written and read only under the owning semaphore's Gate.
|
||||||
|
public int? Result { get; set; }
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string GetSemaphoreWakeKey(uint handle) => $"kernel_sema:0x{handle:X8}";
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "188x57JYp0g",
|
Nid = "188x57JYp0g",
|
||||||
ExportName = "sceKernelCreateSema",
|
ExportName = "sceKernelCreateSema",
|
||||||
@@ -58,17 +71,27 @@ public static class KernelSemaphoreCompatExports
|
|||||||
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
handle = unchecked((uint)Interlocked.Increment(ref _nextSemaphoreHandle));
|
||||||
}
|
}
|
||||||
|
|
||||||
_semaphores[handle] = new KernelSemaphoreState
|
var state = new KernelSemaphoreState
|
||||||
{
|
{
|
||||||
Name = name,
|
Name = name,
|
||||||
InitialCount = initialCount,
|
InitialCount = initialCount,
|
||||||
MaxCount = maxCount,
|
MaxCount = maxCount,
|
||||||
Count = initialCount,
|
Count = initialCount,
|
||||||
};
|
};
|
||||||
|
_semaphores[handle] = state;
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
if (!ctx.TryWriteUInt32(semaphoreAddress, handle))
|
||||||
{
|
{
|
||||||
_semaphores.TryRemove(handle, out _);
|
_semaphores.TryRemove(handle, out _);
|
||||||
|
// Handles are sequential and guest-predictable, so a hostile guest can
|
||||||
|
// race a WaitSema onto the handle between publication above and this
|
||||||
|
// rollback. Strand-proof that waiter exactly like DeleteSema does.
|
||||||
|
lock (state.Gate)
|
||||||
|
{
|
||||||
|
state.Deleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -118,7 +141,17 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!GuestThreadExecution.RequestCurrentThreadBlock(ctx, "sceKernelWaitSema"))
|
var waiter = new SemaphoreWaiter
|
||||||
|
{
|
||||||
|
NeedCount = needCount,
|
||||||
|
CancelEpochAtBlock = semaphore.CancelEpoch,
|
||||||
|
};
|
||||||
|
if (!GuestThreadExecution.RequestCurrentThreadBlock(
|
||||||
|
ctx,
|
||||||
|
"sceKernelWaitSema",
|
||||||
|
GetSemaphoreWakeKey(handle),
|
||||||
|
resumeHandler: () => CompleteBlockedSemaWait(semaphore, waiter),
|
||||||
|
wakeHandler: () => TryConsumeBlockedSemaWait(semaphore, waiter)))
|
||||||
{
|
{
|
||||||
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
TraceSemaphore($"wait-would-block handle=0x{handle:X8} name='{semaphore.Name}' need={needCount} count={semaphore.Count}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN);
|
||||||
@@ -193,8 +226,14 @@ public static class KernelSemaphoreCompatExports
|
|||||||
|
|
||||||
semaphore.Count += signalCount;
|
semaphore.Count += signalCount;
|
||||||
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
TraceSemaphore($"signal handle=0x{handle:X8} name='{semaphore.Name}' signal={signalCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wake after releasing the gate (lock order: scheduler gate -> semaphore gate).
|
||||||
|
// Wake everyone; the wake handler consumes the count per waiter, so a waiter
|
||||||
|
// whose needCount exceeds the remaining count stays parked while a smaller
|
||||||
|
// waiter can proceed.
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -226,10 +265,16 @@ public static class KernelSemaphoreCompatExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
semaphore.Count = setCount < 0 ? semaphore.InitialCount : setCount;
|
||||||
semaphore.WaitingThreads = 0;
|
semaphore.CancelEpoch++;
|
||||||
|
// WaitingThreads is NOT zeroed here: each canceled waiter decrements it
|
||||||
|
// exactly once in its wake handler. Zeroing here as well would double-count
|
||||||
|
// and silently absorb the increment of a waiter that parks between this
|
||||||
|
// gate release and the wake-all below.
|
||||||
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
TraceSemaphore($"cancel handle=0x{handle:X8} name='{semaphore.Name}' set={setCount} count={semaphore.Count}");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||||
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -245,10 +290,84 @@ public static class KernelSemaphoreCompatExports
|
|||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete succeeds even with blocked waiters; they wake with the deleted
|
||||||
|
// result (the SCE kernel wakes waiters with the EACCES-class code).
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
semaphore.Deleted = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = GuestThreadExecution.Scheduler?.WakeBlockedThreads(GetSemaphoreWakeKey(handle));
|
||||||
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
TraceSemaphore($"delete handle=0x{handle:X8} name='{semaphore.Name}'");
|
||||||
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Wake handler: runs under the scheduler's guest-thread gate (lock order:
|
||||||
|
// scheduler gate -> semaphore gate). Returns true iff the waiter has a final
|
||||||
|
// result and should be re-readied; false leaves it parked.
|
||||||
|
private static bool TryConsumeBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
||||||
|
{
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
return TryConsumeBlockedSemaWaitLocked(semaphore, waiter);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryConsumeBlockedSemaWaitLocked(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
||||||
|
{
|
||||||
|
if (waiter.Result is not null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semaphore.Deleted)
|
||||||
|
{
|
||||||
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_DELETED;
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
TraceSemaphore($"wake-deleted name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semaphore.CancelEpoch != waiter.CancelEpochAtBlock)
|
||||||
|
{
|
||||||
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_CANCELED;
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
TraceSemaphore($"wake-canceled name='{semaphore.Name}' need={waiter.NeedCount}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (semaphore.Count >= waiter.NeedCount)
|
||||||
|
{
|
||||||
|
semaphore.Count -= waiter.NeedCount;
|
||||||
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
TraceSemaphore($"wake-consume name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count} waiters={semaphore.WaitingThreads}");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Resume handler: runs on the woken guest thread outside the scheduler gate;
|
||||||
|
// its return value becomes the guest's RAX for the resumed sceKernelWaitSema.
|
||||||
|
private static int CompleteBlockedSemaWait(KernelSemaphoreState semaphore, SemaphoreWaiter waiter)
|
||||||
|
{
|
||||||
|
lock (semaphore.Gate)
|
||||||
|
{
|
||||||
|
if (waiter.Result is null && !TryConsumeBlockedSemaWaitLocked(semaphore, waiter))
|
||||||
|
{
|
||||||
|
// Nothing readies a parked semaphore waiter without the wake handler
|
||||||
|
// resolving it, so reaching here means the scheduler contract changed.
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][GAP] sema.resume-no-outcome name='{semaphore.Name}' need={waiter.NeedCount} count={semaphore.Count}");
|
||||||
|
waiter.Result = (int)OrbisGen2Result.ORBIS_GEN2_ERROR_TRY_AGAIN;
|
||||||
|
semaphore.WaitingThreads = Math.Max(0, semaphore.WaitingThreads - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
return waiter.Result!.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static void TraceSemaphore(string message)
|
private static void TraceSemaphore(string message)
|
||||||
{
|
{
|
||||||
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_SEMA"), "1", StringComparison.Ordinal))
|
||||||
|
|||||||
@@ -0,0 +1,467 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Buffers;
|
||||||
|
using System.Collections.Concurrent;
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using SharpEmu.Libs.Kernel;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.LibcStdio;
|
||||||
|
|
||||||
|
public static class LibcStdioExports
|
||||||
|
{
|
||||||
|
private const int MaxPathLength = 4096;
|
||||||
|
private const int MaxModeLength = 16;
|
||||||
|
private const int ReadChunkSize = 1024 * 1024;
|
||||||
|
|
||||||
|
private static readonly ConcurrentDictionary<ulong, FileStream> _fileHandles = new();
|
||||||
|
private static long _nextHandle = 0x1000 - 8;
|
||||||
|
|
||||||
|
private static readonly bool _traceStdio =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_STDIO"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
|
private const int CtypeTableLowerBound = -128;
|
||||||
|
private const int CtypeTableUpperBound = 255;
|
||||||
|
private const int CtypeTableEntryCount = CtypeTableUpperBound - CtypeTableLowerBound + 1; // 384 entries * 2 bytes = 768 bytes
|
||||||
|
|
||||||
|
// Mirrors the Dinkumware ctype bitmask layout (the CRT Sony's libc is based on;
|
||||||
|
// _Getpctype/_Getptolower/_Getptoupper are Dinkumware accessor names). This is NOT the
|
||||||
|
// MSVC/UCRT layout: e.g. Dinkumware puts digit at 0x20 where UCRT puts control, so
|
||||||
|
// serving a UCRT-shaped table made the game's bundled printf engine misparse every
|
||||||
|
// %-directive (fatal-error messages rendered empty) and made its mcpp preprocessor
|
||||||
|
// treat 'a'-'f' as control characters (UCRT _HEX=0x80 reads as Dinkumware _BB) and
|
||||||
|
// drop them from identifiers ("texture" -> "txtur"). Titles that bundle their own
|
||||||
|
// libc bypass this table entirely (see IsSafeLleLibcExport in DirectExecutionBackend);
|
||||||
|
// this fallback only serves titles that import _Getpctype without shipping one.
|
||||||
|
private const ushort CtypeXDigit = 0x001; // _XD '0'-'9', 'A'-'F', 'a'-'f'
|
||||||
|
private const ushort CtypeUpper = 0x002; // _UP 'A'-'Z'
|
||||||
|
private const ushort CtypeSpace = 0x004; // _SP ' ' (isspace = _CN|_SP|_XS)
|
||||||
|
private const ushort CtypePunct = 0x008; // _PU punctuation
|
||||||
|
private const ushort CtypeLower = 0x010; // _LO 'a'-'z'
|
||||||
|
private const ushort CtypeDigit = 0x020; // _DI '0'-'9'
|
||||||
|
private const ushort CtypeControlSpace = 0x040; // _CN '\t','\n','\v','\f','\r'
|
||||||
|
private const ushort CtypeControl = 0x080; // _BB other control characters
|
||||||
|
private const ushort CtypeBlank = 0x400; // _XB ' ' and '\t'
|
||||||
|
|
||||||
|
private static readonly object _ctypeTableGate = new();
|
||||||
|
private static nint _ctypeTableBase;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "xeYO4u7uyJ0",
|
||||||
|
ExportName = "fopen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Fopen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var pathAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var modeAddress = ctx[CpuRegister.Rsi];
|
||||||
|
|
||||||
|
if (pathAddress == 0 || modeAddress == 0 ||
|
||||||
|
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, pathAddress, MaxPathLength, out var guestPath) ||
|
||||||
|
!KernelMemoryCompatExports.TryReadNullTerminatedUtf8(ctx, modeAddress, MaxModeLength, out var mode) ||
|
||||||
|
!TryParseFopenMode(mode, out var fileMode, out var fileAccess))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var hostPath = KernelMemoryCompatExports.ResolveGuestPath(guestPath);
|
||||||
|
if (fileAccess != FileAccess.Read && KernelMemoryCompatExports.IsReadOnlyGuestMutationPath(guestPath))
|
||||||
|
{
|
||||||
|
if (_traceStdio)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> PERMISSION_DENIED (read-only path)");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
if (fileAccess != FileAccess.Read)
|
||||||
|
{
|
||||||
|
var parentDirectory = Path.GetDirectoryName(hostPath);
|
||||||
|
if (!string.IsNullOrWhiteSpace(parentDirectory))
|
||||||
|
{
|
||||||
|
Directory.CreateDirectory(parentDirectory);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var stream = new FileStream(hostPath, fileMode, fileAccess, FileShare.ReadWrite);
|
||||||
|
if (mode.StartsWith('a') && fileAccess == FileAccess.ReadWrite)
|
||||||
|
{
|
||||||
|
stream.Seek(0, SeekOrigin.End);
|
||||||
|
}
|
||||||
|
|
||||||
|
var handle = unchecked((ulong)Interlocked.Add(ref _nextHandle, 8));
|
||||||
|
_fileHandles[handle] = stream;
|
||||||
|
|
||||||
|
if (_traceStdio)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> OK handle=0x{handle:X} length={stream.Length}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = handle;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
catch (Exception ex) when (ex is IOException or UnauthorizedAccessException)
|
||||||
|
{
|
||||||
|
if (_traceStdio)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] fopen: guest='{guestPath}' host='{hostPath}' mode='{mode}' -> FAILED {ex.GetType().Name}: {ex.Message}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return ex is UnauthorizedAccessException
|
||||||
|
? (int)OrbisGen2Result.ORBIS_GEN2_ERROR_PERMISSION_DENIED
|
||||||
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rQFVBXp-Cxg",
|
||||||
|
ExportName = "fseek",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Fseek(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
var offset = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||||
|
var whence = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
|
||||||
|
if (!_fileHandles.TryGetValue(handle, out var stream) || !TryGetSeekOrigin(whence, out var origin))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream.Seek(offset, origin);
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Qazy8LmXTvw",
|
||||||
|
ExportName = "ftell",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Ftell(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
|
||||||
|
if (!_fileHandles.TryGetValue(handle, out var stream))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(long)-1);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)stream.Position);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(long)-1);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "uodLYyUip20",
|
||||||
|
ExportName = "fclose",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Fclose(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = ctx[CpuRegister.Rdi];
|
||||||
|
|
||||||
|
if (!_fileHandles.TryRemove(handle, out var stream))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
stream.Dispose();
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)-1);
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "lbB+UlZqVG0",
|
||||||
|
ExportName = "fread",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Fread(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destination = ctx[CpuRegister.Rdi];
|
||||||
|
var elementSize = ctx[CpuRegister.Rsi];
|
||||||
|
var elementCount = ctx[CpuRegister.Rdx];
|
||||||
|
var handle = ctx[CpuRegister.Rcx];
|
||||||
|
|
||||||
|
if (elementSize == 0 || elementCount == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (destination == 0 || !_fileHandles.TryGetValue(handle, out var stream))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var totalRequested = elementSize * elementCount;
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent((int)Math.Min((ulong)ReadChunkSize, totalRequested));
|
||||||
|
ulong totalRead = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (totalRead < totalRequested)
|
||||||
|
{
|
||||||
|
var request = (int)Math.Min((ulong)buffer.Length, totalRequested - totalRead);
|
||||||
|
var read = stream.Read(buffer, 0, request);
|
||||||
|
if (read <= 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.Memory.TryWrite(destination + totalRead, buffer.AsSpan(0, read)))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = totalRead / elementSize;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
totalRead += (ulong)read;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = totalRead / elementSize;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_traceStdio)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] fread: handle=0x{handle:X} requested={totalRequested} read={totalRead} pos={stream.Position}");
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = totalRead / elementSize;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "KdP-nULpuGw",
|
||||||
|
ExportName = "fgets",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int Fgets(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var destination = ctx[CpuRegister.Rdi];
|
||||||
|
var maxCount = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var handle = ctx[CpuRegister.Rdx];
|
||||||
|
|
||||||
|
if (destination == 0 || maxCount <= 0 || !_fileHandles.TryGetValue(handle, out var stream))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var buffer = ArrayPool<byte>.Shared.Rent(maxCount - 1);
|
||||||
|
var count = 0;
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (count < maxCount - 1)
|
||||||
|
{
|
||||||
|
var b = stream.ReadByte();
|
||||||
|
if (b < 0)
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
buffer[count++] = (byte)b;
|
||||||
|
if (b == '\n')
|
||||||
|
{
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (count == 0)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> withNul = stackalloc byte[count + 1];
|
||||||
|
buffer.AsSpan(0, count).CopyTo(withNul);
|
||||||
|
withNul[count] = 0;
|
||||||
|
|
||||||
|
if (!ctx.Memory.TryWrite(destination, withNul))
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
ArrayPool<byte>.Shared.Return(buffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx[CpuRegister.Rax] = destination;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sUP1hBaouOw",
|
||||||
|
ExportName = "_Getpctype",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libc")]
|
||||||
|
public static int GetPctype(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)EnsureCtypeTable());
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static unsafe nint EnsureCtypeTable()
|
||||||
|
{
|
||||||
|
lock (_ctypeTableGate)
|
||||||
|
{
|
||||||
|
if (_ctypeTableBase != 0)
|
||||||
|
{
|
||||||
|
return _ctypeTableBase;
|
||||||
|
}
|
||||||
|
|
||||||
|
var storage = Marshal.AllocHGlobal(CtypeTableEntryCount * sizeof(ushort));
|
||||||
|
var entries = new Span<ushort>((void*)storage, CtypeTableEntryCount);
|
||||||
|
for (var i = 0; i < CtypeTableEntryCount; i++)
|
||||||
|
{
|
||||||
|
var c = i + CtypeTableLowerBound;
|
||||||
|
entries[i] = c is >= 0 and <= 0x7F ? ComputeCtypeFlags(c) : (ushort)0;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Table is indexed as base[c] for c in [-128, 255], so the pointer handed to the
|
||||||
|
// guest must point at the c == 0 entry, not the start of the allocation.
|
||||||
|
_ctypeTableBase = storage - (CtypeTableLowerBound * sizeof(ushort));
|
||||||
|
return _ctypeTableBase;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static ushort ComputeCtypeFlags(int c)
|
||||||
|
{
|
||||||
|
var isUpper = c is >= 'A' and <= 'Z';
|
||||||
|
var isLower = c is >= 'a' and <= 'z';
|
||||||
|
var isDigit = c is >= '0' and <= '9';
|
||||||
|
var isHex = isDigit || (c is >= 'A' and <= 'F') || (c is >= 'a' and <= 'f');
|
||||||
|
var isAlnum = isUpper || isLower || isDigit;
|
||||||
|
// In the Dinkumware table '\t'..'\r' carry _CN (isspace and iscntrl both match via
|
||||||
|
// _CN) while plain ' ' carries _SP; keeping them distinct is what keeps isprint(' ')
|
||||||
|
// true and isprint('\t') false.
|
||||||
|
var isControlSpace = c is >= 0x09 and <= 0x0D;
|
||||||
|
var isControl = (c is >= 0x00 and <= 0x08) || (c is >= 0x0E and <= 0x1F) || c == 0x7F;
|
||||||
|
var isPrintable = c is >= 0x20 and <= 0x7E;
|
||||||
|
var isPunct = isPrintable && !isAlnum && c != ' ';
|
||||||
|
|
||||||
|
ushort flags = 0;
|
||||||
|
if (isUpper) flags |= CtypeUpper;
|
||||||
|
if (isLower) flags |= CtypeLower;
|
||||||
|
if (isDigit) flags |= CtypeDigit;
|
||||||
|
if (isHex) flags |= CtypeXDigit;
|
||||||
|
if (c == ' ') flags |= CtypeSpace | CtypeBlank;
|
||||||
|
if (c == '\t') flags |= CtypeBlank;
|
||||||
|
if (isControlSpace) flags |= CtypeControlSpace;
|
||||||
|
if (isPunct) flags |= CtypePunct;
|
||||||
|
if (isControl) flags |= CtypeControl;
|
||||||
|
return flags;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryParseFopenMode(string mode, out FileMode fileMode, out FileAccess fileAccess)
|
||||||
|
{
|
||||||
|
fileMode = FileMode.Open;
|
||||||
|
fileAccess = FileAccess.Read;
|
||||||
|
|
||||||
|
if (string.IsNullOrEmpty(mode))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
var plus = mode.Contains('+');
|
||||||
|
switch (mode[0])
|
||||||
|
{
|
||||||
|
case 'r':
|
||||||
|
fileMode = FileMode.Open;
|
||||||
|
fileAccess = plus ? FileAccess.ReadWrite : FileAccess.Read;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'w':
|
||||||
|
fileMode = FileMode.Create;
|
||||||
|
fileAccess = plus ? FileAccess.ReadWrite : FileAccess.Write;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 'a':
|
||||||
|
if (plus)
|
||||||
|
{
|
||||||
|
fileMode = FileMode.OpenOrCreate;
|
||||||
|
fileAccess = FileAccess.ReadWrite;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
fileMode = FileMode.Append;
|
||||||
|
fileAccess = FileAccess.Write;
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryGetSeekOrigin(int whence, out SeekOrigin origin)
|
||||||
|
{
|
||||||
|
switch (whence)
|
||||||
|
{
|
||||||
|
case 0:
|
||||||
|
origin = SeekOrigin.Begin;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 1:
|
||||||
|
origin = SeekOrigin.Current;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
case 2:
|
||||||
|
origin = SeekOrigin.End;
|
||||||
|
return true;
|
||||||
|
|
||||||
|
default:
|
||||||
|
origin = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,33 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Mouse;
|
||||||
|
|
||||||
|
public static class MouseExports
|
||||||
|
{
|
||||||
|
// Returns 0 read entries: no mouse is connected. This NID was previously misbound
|
||||||
|
// as an sceNgs2VoiceGetState alias.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "x8qnXqh-tiM",
|
||||||
|
ExportName = "sceMouseRead",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceMouse")]
|
||||||
|
public static int MouseRead(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "RaqxZIf6DvE",
|
||||||
|
ExportName = "sceMouseOpen",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceMouse")]
|
||||||
|
public static int MouseOpen(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,7 +13,7 @@ public static class Ngs2Exports
|
|||||||
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
|
private const int OrbisNgs2ErrorInvalidSystemHandle = unchecked((int)0x804A0230);
|
||||||
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
|
private const int OrbisNgs2ErrorInvalidRackHandle = unchecked((int)0x804A0261);
|
||||||
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
|
private const int OrbisNgs2ErrorInvalidVoiceHandle = unchecked((int)0x804A0300);
|
||||||
private const ulong HandleStorageSize = 0x20;
|
private const ulong HandleStorageSize = 0x1000;
|
||||||
private const int RenderBufferInfoSize = 0x18;
|
private const int RenderBufferInfoSize = 0x18;
|
||||||
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
|
private const ulong MaximumRenderBufferSize = 16 * 1024 * 1024;
|
||||||
|
|
||||||
@@ -197,12 +197,18 @@ public static class Ngs2Exports
|
|||||||
lock (StateGate)
|
lock (StateGate)
|
||||||
{
|
{
|
||||||
return ctx.SetReturn(
|
return ctx.SetReturn(
|
||||||
Voices.ContainsKey(ctx[CpuRegister.Rdi])
|
Voices.ContainsKey(ctx[CpuRegister.Rdi])
|
||||||
? 0
|
? 0
|
||||||
: OrbisNgs2ErrorInvalidVoiceHandle);
|
: OrbisNgs2ErrorInvalidVoiceHandle);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// The earlier "alt NID" guesses here were wrong: an NID is the aerolib hash of one
|
||||||
|
// symbol name, and hashing scripts/ps5_names.txt shows the previous alt bindings
|
||||||
|
// actually belonged to sceImeUpdate (-4GCfYdNF1s), sceMouseRead (x8qnXqh-tiM) and
|
||||||
|
// sceSystemGestureUpdateAllTouchRecognizer (wPJGwI2RM2I) — Quake's audio thread was
|
||||||
|
// receiving an NGS2 error from what it thought was sceImeUpdate. Those now live in
|
||||||
|
// their real libraries; the NIDs below are verified against the hash.
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "AbYvTOZ8Pts",
|
Nid = "AbYvTOZ8Pts",
|
||||||
ExportName = "sceNgs2VoiceRunCommands",
|
ExportName = "sceNgs2VoiceRunCommands",
|
||||||
@@ -210,6 +216,27 @@ public static class Ngs2Exports
|
|||||||
LibraryName = "libSceNgs2")]
|
LibraryName = "libSceNgs2")]
|
||||||
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
public static int Ngs2VoiceRunCommands(CpuContext ctx) => Ngs2VoiceControl(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-TOuuAQ-buE",
|
||||||
|
ExportName = "sceNgs2VoiceGetState",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2VoiceGetState(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "rEh728kXk3w",
|
||||||
|
ExportName = "sceNgs2VoiceGetStateFlags",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2VoiceGetStateFlags(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "xa8oL9dmXkM",
|
||||||
|
ExportName = "sceNgs2PanInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNgs2")]
|
||||||
|
public static int Ngs2PanInit(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "i0VnXM-C9fc",
|
Nid = "i0VnXM-C9fc",
|
||||||
ExportName = "sceNgs2SystemRender",
|
ExportName = "sceNgs2SystemRender",
|
||||||
@@ -263,7 +290,6 @@ public static class Ngs2Exports
|
|||||||
|
|
||||||
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
private static bool TryCreateHandle(CpuContext ctx, uint type, ulong ownerHandle, out ulong handle)
|
||||||
{
|
{
|
||||||
handle = 0;
|
|
||||||
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
|
if (!KernelMemoryCompatExports.TryAllocateHleData(ctx, HandleStorageSize, 16, out handle))
|
||||||
{
|
{
|
||||||
return false;
|
return false;
|
||||||
@@ -271,11 +297,18 @@ public static class Ngs2Exports
|
|||||||
|
|
||||||
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
|
Span<byte> data = stackalloc byte[(int)HandleStorageSize];
|
||||||
data.Clear();
|
data.Clear();
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(data[0..8], handle);
|
|
||||||
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
|
BinaryPrimitives.WriteUInt64LittleEndian(data[8..16], ownerHandle);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
|
BinaryPrimitives.WriteUInt32LittleEndian(data[16..20], 1);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
|
BinaryPrimitives.WriteUInt32LittleEndian(data[24..28], type);
|
||||||
return ctx.Memory.TryWrite(handle, data);
|
if (!ctx.Memory.TryWrite(handle, data))
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Offset 0 doubles as a vtable slot for guest code that treats this handle as a C++
|
||||||
|
// object; stamp it with the shared dummy vtable instead of a self-referential value.
|
||||||
|
KernelMemoryCompatExports.TryWriteDummyVtable(ctx, handle);
|
||||||
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
private static bool TryClearGuestBuffer(CpuContext ctx, ulong address, ulong length)
|
||||||
|
|||||||
@@ -33,6 +33,30 @@ public static class NpManagerExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Offline profile: the online id payload is left untouched and the call
|
||||||
|
// reports success, matching the other offline NpManager stubs here.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "XDncXQIJUSk",
|
||||||
|
ExportName = "sceNpGetOnlineId",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpGetOnlineId(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "e-ZuhGEoeC4",
|
||||||
|
ExportName = "sceNpGetNpReachabilityState",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpManager")]
|
||||||
|
public static int NpGetNpReachabilityState(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "VfRSmPmj8Q8",
|
Nid = "VfRSmPmj8Q8",
|
||||||
ExportName = "sceNpRegisterStateCallback",
|
ExportName = "sceNpRegisterStateCallback",
|
||||||
|
|||||||
@@ -0,0 +1,108 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
using System.Buffers.Binary;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Np;
|
||||||
|
|
||||||
|
public static class NpTrophy2Exports
|
||||||
|
{
|
||||||
|
private static int _nextContext = 1;
|
||||||
|
private static int _nextHandle = 1;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Bagshr7OQ6Q",
|
||||||
|
ExportName = "sceNpTrophy2CreateContext",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpTrophy2")]
|
||||||
|
public static int NpTrophy2CreateContext(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return WriteIdAndReturn(ctx, ctx[CpuRegister.Rdi], ref _nextContext);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Gz1rmUZpROM",
|
||||||
|
ExportName = "sceNpTrophy2CreateHandle",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpTrophy2")]
|
||||||
|
public static int NpTrophy2CreateHandle(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return WriteIdAndReturn(ctx, ctx[CpuRegister.Rdi], ref _nextHandle);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sysY2FHYff4",
|
||||||
|
ExportName = "sceNpTrophy2DestroyContext",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpTrophy2")]
|
||||||
|
public static int NpTrophy2DestroyContext(CpuContext ctx) => ReturnOk(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "d8P11CI40KE",
|
||||||
|
ExportName = "sceNpTrophy2DestroyHandle",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpTrophy2")]
|
||||||
|
public static int NpTrophy2DestroyHandle(CpuContext ctx) => ReturnOk(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "fYapWA9xVmA",
|
||||||
|
ExportName = "sceNpTrophy2AbortHandle",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpTrophy2")]
|
||||||
|
public static int NpTrophy2AbortHandle(CpuContext ctx) => ReturnOk(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "bIDov3wBu5Q",
|
||||||
|
ExportName = "sceNpTrophy2RegisterContext",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpTrophy2")]
|
||||||
|
public static int NpTrophy2RegisterContext(CpuContext ctx) => ReturnOk(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "sUXGfNMalIo",
|
||||||
|
ExportName = "sceNpTrophy2RegisterUnlockCallback",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpTrophy2")]
|
||||||
|
public static int NpTrophy2RegisterUnlockCallback(CpuContext ctx) => ReturnOk(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "wVqxM58sIKs",
|
||||||
|
ExportName = "sceNpTrophy2UnregisterUnlockCallback",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpTrophy2")]
|
||||||
|
public static int NpTrophy2UnregisterUnlockCallback(CpuContext ctx) => ReturnOk(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "EHQEDVXZ0TI",
|
||||||
|
ExportName = "sceNpTrophy2ShowTrophyList",
|
||||||
|
Target = Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpTrophy2")]
|
||||||
|
public static int NpTrophy2ShowTrophyList(CpuContext ctx) => ReturnOk(ctx);
|
||||||
|
|
||||||
|
private static int WriteIdAndReturn(CpuContext ctx, ulong outAddress, ref int nextId)
|
||||||
|
{
|
||||||
|
if (outAddress == 0)
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
|
||||||
|
}
|
||||||
|
|
||||||
|
Span<byte> idBytes = stackalloc byte[sizeof(int)];
|
||||||
|
BinaryPrimitives.WriteInt32LittleEndian(idBytes, nextId);
|
||||||
|
if (!ctx.Memory.TryWrite(outAddress, idBytes))
|
||||||
|
{
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
|
}
|
||||||
|
|
||||||
|
nextId++;
|
||||||
|
return SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ReturnOk(CpuContext ctx) => SetReturn(ctx, OrbisGen2Result.ORBIS_GEN2_OK);
|
||||||
|
|
||||||
|
private static int SetReturn(CpuContext ctx, OrbisGen2Result result)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||||
|
return (int)result;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -76,4 +76,14 @@ public static class NpUniversalDataSystemExports
|
|||||||
{
|
{
|
||||||
return ctx.SetReturn(0, typeof(long));
|
return ctx.SetReturn(0, typeof(long));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "AUIHb7jUX3I",
|
||||||
|
ExportName = "sceNpUniversalDataSystemDestroyHandle",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceNpUniversalDataSystem")]
|
||||||
|
public static int NpUniversalDataSystemDestroyHandle(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(0, typeof(long));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,21 +5,6 @@ using Microsoft.Win32.SafeHandles;
|
|||||||
|
|
||||||
namespace SharpEmu.Libs.Pad;
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
/// <summary>
|
|
||||||
/// Snapshot of the DualSense state, already translated to ORBIS pad
|
|
||||||
/// conventions (SCE_PAD_BUTTON bits; sticks 0..255 with 128 centered;
|
|
||||||
/// triggers 0..255).
|
|
||||||
/// </summary>
|
|
||||||
internal readonly record struct DualSenseState(
|
|
||||||
bool Connected,
|
|
||||||
uint Buttons,
|
|
||||||
byte LeftX,
|
|
||||||
byte LeftY,
|
|
||||||
byte RightX,
|
|
||||||
byte RightY,
|
|
||||||
byte L2,
|
|
||||||
byte R2);
|
|
||||||
|
|
||||||
/// <summary>
|
/// <summary>
|
||||||
/// Reads a DualSense controller over raw HID on a background thread.
|
/// Reads a DualSense controller over raw HID on a background thread.
|
||||||
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
|
/// Supports USB (input report 0x01) and Bluetooth (extended report 0x31,
|
||||||
@@ -31,26 +16,8 @@ internal static class DualSenseReader
|
|||||||
private const ushort DualSenseProductId = 0x0CE6;
|
private const ushort DualSenseProductId = 0x0CE6;
|
||||||
private const ushort DualSenseEdgeProductId = 0x0DF2;
|
private const ushort DualSenseEdgeProductId = 0x0DF2;
|
||||||
|
|
||||||
// SCE_PAD_BUTTON bit values.
|
|
||||||
private const uint ButtonL3 = 0x0002;
|
|
||||||
private const uint ButtonR3 = 0x0004;
|
|
||||||
private const uint ButtonOptions = 0x0008;
|
|
||||||
private const uint ButtonUp = 0x0010;
|
|
||||||
private const uint ButtonRight = 0x0020;
|
|
||||||
private const uint ButtonDown = 0x0040;
|
|
||||||
private const uint ButtonLeft = 0x0080;
|
|
||||||
private const uint ButtonL2 = 0x0100;
|
|
||||||
private const uint ButtonR2 = 0x0200;
|
|
||||||
private const uint ButtonL1 = 0x0400;
|
|
||||||
private const uint ButtonR1 = 0x0800;
|
|
||||||
private const uint ButtonTriangle = 0x1000;
|
|
||||||
private const uint ButtonCircle = 0x2000;
|
|
||||||
private const uint ButtonCross = 0x4000;
|
|
||||||
private const uint ButtonSquare = 0x8000;
|
|
||||||
private const uint ButtonTouchPad = 0x100000;
|
|
||||||
|
|
||||||
private static readonly object Gate = new();
|
private static readonly object Gate = new();
|
||||||
private static DualSenseState _state;
|
private static PadState _state;
|
||||||
private static bool _started;
|
private static bool _started;
|
||||||
|
|
||||||
// Output (rumble/lightbar) state, all guarded by Gate.
|
// Output (rumble/lightbar) state, all guarded by Gate.
|
||||||
@@ -92,7 +59,7 @@ internal static class DualSenseReader
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static bool TryGetState(out DualSenseState state)
|
internal static bool TryGetState(out PadState state)
|
||||||
{
|
{
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
@@ -102,7 +69,7 @@ internal static class DualSenseReader
|
|||||||
return state.Connected;
|
return state.Connected;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static void SetState(in DualSenseState state)
|
private static void SetState(in PadState state)
|
||||||
{
|
{
|
||||||
lock (Gate)
|
lock (Gate)
|
||||||
{
|
{
|
||||||
@@ -399,7 +366,7 @@ internal static class DualSenseReader
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryParseReport(ReadOnlySpan<byte> report, out DualSenseState state)
|
private static bool TryParseReport(ReadOnlySpan<byte> report, out PadState state)
|
||||||
{
|
{
|
||||||
// USB: report id 0x01, payload starts at [1].
|
// USB: report id 0x01, payload starts at [1].
|
||||||
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
|
// Bluetooth extended: report id 0x31, sequence byte at [1], payload at [2].
|
||||||
@@ -429,21 +396,21 @@ internal static class DualSenseReader
|
|||||||
var buttons2 = report[offset + 9];
|
var buttons2 = report[offset + 9];
|
||||||
|
|
||||||
uint buttons = 0;
|
uint buttons = 0;
|
||||||
buttons |= (buttons0 & 0x10) != 0 ? ButtonSquare : 0;
|
buttons |= (buttons0 & 0x10) != 0 ? OrbisPadButton.Square : 0;
|
||||||
buttons |= (buttons0 & 0x20) != 0 ? ButtonCross : 0;
|
buttons |= (buttons0 & 0x20) != 0 ? OrbisPadButton.Cross : 0;
|
||||||
buttons |= (buttons0 & 0x40) != 0 ? ButtonCircle : 0;
|
buttons |= (buttons0 & 0x40) != 0 ? OrbisPadButton.Circle : 0;
|
||||||
buttons |= (buttons0 & 0x80) != 0 ? ButtonTriangle : 0;
|
buttons |= (buttons0 & 0x80) != 0 ? OrbisPadButton.Triangle : 0;
|
||||||
buttons |= HatToButtons(buttons0 & 0x0F);
|
buttons |= HatToButtons(buttons0 & 0x0F);
|
||||||
buttons |= (buttons1 & 0x01) != 0 ? ButtonL1 : 0;
|
buttons |= (buttons1 & 0x01) != 0 ? OrbisPadButton.L1 : 0;
|
||||||
buttons |= (buttons1 & 0x02) != 0 ? ButtonR1 : 0;
|
buttons |= (buttons1 & 0x02) != 0 ? OrbisPadButton.R1 : 0;
|
||||||
buttons |= (buttons1 & 0x04) != 0 ? ButtonL2 : 0;
|
buttons |= (buttons1 & 0x04) != 0 ? OrbisPadButton.L2 : 0;
|
||||||
buttons |= (buttons1 & 0x08) != 0 ? ButtonR2 : 0;
|
buttons |= (buttons1 & 0x08) != 0 ? OrbisPadButton.R2 : 0;
|
||||||
buttons |= (buttons1 & 0x20) != 0 ? ButtonOptions : 0;
|
buttons |= (buttons1 & 0x20) != 0 ? OrbisPadButton.Options : 0;
|
||||||
buttons |= (buttons1 & 0x40) != 0 ? ButtonL3 : 0;
|
buttons |= (buttons1 & 0x40) != 0 ? OrbisPadButton.L3 : 0;
|
||||||
buttons |= (buttons1 & 0x80) != 0 ? ButtonR3 : 0;
|
buttons |= (buttons1 & 0x80) != 0 ? OrbisPadButton.R3 : 0;
|
||||||
buttons |= (buttons2 & 0x02) != 0 ? ButtonTouchPad : 0;
|
buttons |= (buttons2 & 0x02) != 0 ? OrbisPadButton.TouchPad : 0;
|
||||||
|
|
||||||
state = new DualSenseState(
|
state = new PadState(
|
||||||
Connected: true,
|
Connected: true,
|
||||||
Buttons: buttons,
|
Buttons: buttons,
|
||||||
LeftX: leftX,
|
LeftX: leftX,
|
||||||
@@ -457,14 +424,14 @@ internal static class DualSenseReader
|
|||||||
|
|
||||||
private static uint HatToButtons(int hat) => hat switch
|
private static uint HatToButtons(int hat) => hat switch
|
||||||
{
|
{
|
||||||
0 => ButtonUp,
|
0 => OrbisPadButton.Up,
|
||||||
1 => ButtonUp | ButtonRight,
|
1 => OrbisPadButton.Up | OrbisPadButton.Right,
|
||||||
2 => ButtonRight,
|
2 => OrbisPadButton.Right,
|
||||||
3 => ButtonRight | ButtonDown,
|
3 => OrbisPadButton.Right | OrbisPadButton.Down,
|
||||||
4 => ButtonDown,
|
4 => OrbisPadButton.Down,
|
||||||
5 => ButtonDown | ButtonLeft,
|
5 => OrbisPadButton.Down | OrbisPadButton.Left,
|
||||||
6 => ButtonLeft,
|
6 => OrbisPadButton.Left,
|
||||||
7 => ButtonLeft | ButtonUp,
|
7 => OrbisPadButton.Left | OrbisPadButton.Up,
|
||||||
_ => 0,
|
_ => 0,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -31,6 +31,7 @@ public static class PadExports
|
|||||||
{
|
{
|
||||||
_initialized = true;
|
_initialized = true;
|
||||||
DualSenseReader.EnsureStarted();
|
DualSenseReader.EnsureStarted();
|
||||||
|
XInputReader.EnsureStarted();
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -61,9 +62,12 @@ public static class PadExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
DualSenseReader.EnsureStarted();
|
DualSenseReader.EnsureStarted();
|
||||||
|
XInputReader.EnsureStarted();
|
||||||
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
Console.Error.WriteLine(DualSenseReader.TryGetState(out _)
|
||||||
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
? "[LOADER][INFO] Controls: DualSense connected (keyboard fallback also active)."
|
||||||
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense will be used automatically when plugged in.");
|
: XInputReader.TryGetState(out _)
|
||||||
|
? "[LOADER][INFO] Controls: Xbox controller connected (keyboard fallback also active)."
|
||||||
|
: "[LOADER][INFO] Keyboard controls: Arrow keys = D-pad, WASD = left stick, IJKL = right stick, Z/Enter = Cross, X/Esc = Circle, C = Square, V = Triangle, Q = L1, E = R1, R = L2, F = R2, Tab/Backspace = Options. A DualSense or Xbox controller will be used automatically when plugged in.");
|
||||||
return ctx.SetReturn(PrimaryPadHandle);
|
return ctx.SetReturn(PrimaryPadHandle);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -201,6 +205,7 @@ public static class PadExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
DualSenseReader.SetRumble(parameter[0], parameter[1]);
|
DualSenseReader.SetRumble(parameter[0], parameter[1]);
|
||||||
|
XInputReader.SetRumble(parameter[0], parameter[1]);
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -277,6 +282,17 @@ public static class PadExports
|
|||||||
r2 = Math.Max(r2, pad.R2);
|
r2 = Math.Max(r2, pad.R2);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (XInputReader.TryGetState(out var xpad))
|
||||||
|
{
|
||||||
|
buttons |= xpad.Buttons;
|
||||||
|
leftX = MergeAxis(xpad.LeftX, leftX);
|
||||||
|
leftY = MergeAxis(xpad.LeftY, leftY);
|
||||||
|
rightX = MergeAxis(xpad.RightX, rightX);
|
||||||
|
rightY = MergeAxis(xpad.RightY, rightY);
|
||||||
|
l2 = Math.Max(l2, xpad.L2);
|
||||||
|
r2 = Math.Max(r2, xpad.R2);
|
||||||
|
}
|
||||||
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
BinaryPrimitives.WriteUInt32LittleEndian(data[0x00..], buttons);
|
||||||
data[0x04] = leftX;
|
data[0x04] = leftX;
|
||||||
data[0x05] = leftY;
|
data[0x05] = leftY;
|
||||||
|
|||||||
@@ -0,0 +1,40 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Snapshot of a physical controller's state, already translated to ORBIS
|
||||||
|
/// pad conventions (SCE_PAD_BUTTON bits; sticks 0..255 with 128 centered
|
||||||
|
/// and Y growing downward; triggers 0..255).
|
||||||
|
/// </summary>
|
||||||
|
internal readonly record struct PadState(
|
||||||
|
bool Connected,
|
||||||
|
uint Buttons,
|
||||||
|
byte LeftX,
|
||||||
|
byte LeftY,
|
||||||
|
byte RightX,
|
||||||
|
byte RightY,
|
||||||
|
byte L2,
|
||||||
|
byte R2);
|
||||||
|
|
||||||
|
/// <summary>SCE_PAD_BUTTON bit values.</summary>
|
||||||
|
internal static class OrbisPadButton
|
||||||
|
{
|
||||||
|
internal const uint L3 = 0x0002;
|
||||||
|
internal const uint R3 = 0x0004;
|
||||||
|
internal const uint Options = 0x0008;
|
||||||
|
internal const uint Up = 0x0010;
|
||||||
|
internal const uint Right = 0x0020;
|
||||||
|
internal const uint Down = 0x0040;
|
||||||
|
internal const uint Left = 0x0080;
|
||||||
|
internal const uint L2 = 0x0100;
|
||||||
|
internal const uint R2 = 0x0200;
|
||||||
|
internal const uint L1 = 0x0400;
|
||||||
|
internal const uint R1 = 0x0800;
|
||||||
|
internal const uint Triangle = 0x1000;
|
||||||
|
internal const uint Circle = 0x2000;
|
||||||
|
internal const uint Cross = 0x4000;
|
||||||
|
internal const uint Square = 0x8000;
|
||||||
|
internal const uint TouchPad = 0x100000;
|
||||||
|
}
|
||||||
@@ -0,0 +1,246 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Runtime.InteropServices;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Pad;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Reads Xbox 360 / Xbox One (and other XInput-compatible) controllers via
|
||||||
|
/// the Windows XInput API on a background thread, translated to the same
|
||||||
|
/// ORBIS pad conventions as <see cref="DualSenseReader"/>. Supports rumble
|
||||||
|
/// and hot-plug retry; the first connected slot (of four) is used.
|
||||||
|
/// </summary>
|
||||||
|
internal static class XInputReader
|
||||||
|
{
|
||||||
|
private const uint ErrorSuccess = 0;
|
||||||
|
private const int SlotCount = 4;
|
||||||
|
private const byte TriggerThreshold = 30; // XINPUT_GAMEPAD_TRIGGER_THRESHOLD
|
||||||
|
|
||||||
|
// XINPUT_GAMEPAD wButtons bit values.
|
||||||
|
private const ushort XinputDpadUp = 0x0001;
|
||||||
|
private const ushort XinputDpadDown = 0x0002;
|
||||||
|
private const ushort XinputDpadLeft = 0x0004;
|
||||||
|
private const ushort XinputDpadRight = 0x0008;
|
||||||
|
private const ushort XinputStart = 0x0010;
|
||||||
|
private const ushort XinputBack = 0x0020;
|
||||||
|
private const ushort XinputLeftThumb = 0x0040;
|
||||||
|
private const ushort XinputRightThumb = 0x0080;
|
||||||
|
private const ushort XinputLeftShoulder = 0x0100;
|
||||||
|
private const ushort XinputRightShoulder = 0x0200;
|
||||||
|
private const ushort XinputA = 0x1000;
|
||||||
|
private const ushort XinputB = 0x2000;
|
||||||
|
private const ushort XinputX = 0x4000;
|
||||||
|
private const ushort XinputY = 0x8000;
|
||||||
|
|
||||||
|
private static readonly object Gate = new();
|
||||||
|
private static PadState _state;
|
||||||
|
private static bool _started;
|
||||||
|
private static int _slot = -1; // connected XInput user index, -1 when none
|
||||||
|
private static byte _motorLeft;
|
||||||
|
private static byte _motorRight;
|
||||||
|
|
||||||
|
/// <summary>Starts the background reader once; safe to call repeatedly.</summary>
|
||||||
|
internal static void EnsureStarted()
|
||||||
|
{
|
||||||
|
if (!OperatingSystem.IsWindows())
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (_started)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_started = true;
|
||||||
|
var thread = new Thread(ReadLoop)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "XInputReader",
|
||||||
|
};
|
||||||
|
thread.Start();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
internal static bool TryGetState(out PadState state)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
state = _state;
|
||||||
|
}
|
||||||
|
|
||||||
|
return state.Connected;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SetState(in PadState state)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
_state = state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>Sets rumble; large = left/strong motor, small = right/weak.</summary>
|
||||||
|
internal static void SetRumble(byte largeMotor, byte smallMotor)
|
||||||
|
{
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
if (_motorLeft == largeMotor && _motorRight == smallMotor)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_motorLeft = largeMotor;
|
||||||
|
_motorRight = smallMotor;
|
||||||
|
SendRumbleLocked();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void SendRumbleLocked()
|
||||||
|
{
|
||||||
|
if (_slot < 0)
|
||||||
|
{
|
||||||
|
return; // resent on connect
|
||||||
|
}
|
||||||
|
|
||||||
|
var vibration = new XInputVibration
|
||||||
|
{
|
||||||
|
LeftMotorSpeed = (ushort)(_motorLeft * 257), // 0..255 -> 0..65535
|
||||||
|
RightMotorSpeed = (ushort)(_motorRight * 257),
|
||||||
|
};
|
||||||
|
_ = XInputSetState((uint)_slot, ref vibration);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void ReadLoop()
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
while (true)
|
||||||
|
{
|
||||||
|
var slot = FindConnectedSlot();
|
||||||
|
if (slot < 0)
|
||||||
|
{
|
||||||
|
SetState(default);
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
_slot = slot;
|
||||||
|
SendRumbleLocked();
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] XInput (Xbox) controller connected.");
|
||||||
|
while (XInputGetState((uint)slot, out var state) == ErrorSuccess)
|
||||||
|
{
|
||||||
|
SetState(Translate(state.Gamepad));
|
||||||
|
Thread.Sleep(8);
|
||||||
|
}
|
||||||
|
|
||||||
|
Console.Error.WriteLine("[LOADER][INFO] XInput (Xbox) controller disconnected.");
|
||||||
|
lock (Gate)
|
||||||
|
{
|
||||||
|
_slot = -1;
|
||||||
|
_motorLeft = 0;
|
||||||
|
_motorRight = 0;
|
||||||
|
_state = default;
|
||||||
|
}
|
||||||
|
|
||||||
|
Thread.Sleep(1000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (DllNotFoundException)
|
||||||
|
{
|
||||||
|
// XInput unavailable on this system; leave the reader disconnected.
|
||||||
|
}
|
||||||
|
catch (EntryPointNotFoundException)
|
||||||
|
{
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int FindConnectedSlot()
|
||||||
|
{
|
||||||
|
for (var index = 0; index < SlotCount; index++)
|
||||||
|
{
|
||||||
|
if (XInputGetState((uint)index, out _) == ErrorSuccess)
|
||||||
|
{
|
||||||
|
return index;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return -1;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static PadState Translate(in XInputGamepad pad)
|
||||||
|
{
|
||||||
|
uint buttons = 0;
|
||||||
|
buttons |= (pad.Buttons & XinputDpadUp) != 0 ? OrbisPadButton.Up : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputDpadDown) != 0 ? OrbisPadButton.Down : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputDpadLeft) != 0 ? OrbisPadButton.Left : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputDpadRight) != 0 ? OrbisPadButton.Right : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputStart) != 0 ? OrbisPadButton.Options : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputBack) != 0 ? OrbisPadButton.TouchPad : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputLeftThumb) != 0 ? OrbisPadButton.L3 : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputRightThumb) != 0 ? OrbisPadButton.R3 : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputLeftShoulder) != 0 ? OrbisPadButton.L1 : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputRightShoulder) != 0 ? OrbisPadButton.R1 : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputA) != 0 ? OrbisPadButton.Cross : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputB) != 0 ? OrbisPadButton.Circle : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputX) != 0 ? OrbisPadButton.Square : 0;
|
||||||
|
buttons |= (pad.Buttons & XinputY) != 0 ? OrbisPadButton.Triangle : 0;
|
||||||
|
buttons |= pad.LeftTrigger > TriggerThreshold ? OrbisPadButton.L2 : 0;
|
||||||
|
buttons |= pad.RightTrigger > TriggerThreshold ? OrbisPadButton.R2 : 0;
|
||||||
|
|
||||||
|
return new PadState(
|
||||||
|
Connected: true,
|
||||||
|
Buttons: buttons,
|
||||||
|
LeftX: AxisToByte(pad.ThumbLX),
|
||||||
|
LeftY: AxisToByteInverted(pad.ThumbLY),
|
||||||
|
RightX: AxisToByte(pad.ThumbRX),
|
||||||
|
RightY: AxisToByteInverted(pad.ThumbRY),
|
||||||
|
L2: pad.LeftTrigger,
|
||||||
|
R2: pad.RightTrigger);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static byte AxisToByte(short value) => (byte)((value + 32768) >> 8);
|
||||||
|
|
||||||
|
// XInput Y grows upward, ORBIS pads report Y growing downward.
|
||||||
|
private static byte AxisToByteInverted(short value) => (byte)(255 - ((value + 32768) >> 8));
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct XInputGamepad
|
||||||
|
{
|
||||||
|
public ushort Buttons;
|
||||||
|
public byte LeftTrigger;
|
||||||
|
public byte RightTrigger;
|
||||||
|
public short ThumbLX;
|
||||||
|
public short ThumbLY;
|
||||||
|
public short ThumbRX;
|
||||||
|
public short ThumbRY;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct XInputState
|
||||||
|
{
|
||||||
|
public uint PacketNumber;
|
||||||
|
public XInputGamepad Gamepad;
|
||||||
|
}
|
||||||
|
|
||||||
|
[StructLayout(LayoutKind.Sequential)]
|
||||||
|
private struct XInputVibration
|
||||||
|
{
|
||||||
|
public ushort LeftMotorSpeed;
|
||||||
|
public ushort RightMotorSpeed;
|
||||||
|
}
|
||||||
|
|
||||||
|
// xinput1_4.dll ships with Windows 8 and later.
|
||||||
|
[DllImport("xinput1_4.dll")]
|
||||||
|
private static extern uint XInputGetState(uint userIndex, out XInputState state);
|
||||||
|
|
||||||
|
[DllImport("xinput1_4.dll")]
|
||||||
|
private static extern uint XInputSetState(uint userIndex, ref XInputVibration vibration);
|
||||||
|
}
|
||||||
@@ -9,6 +9,189 @@ namespace SharpEmu.Libs.Rtc;
|
|||||||
public static class RtcExports
|
public static class RtcExports
|
||||||
{
|
{
|
||||||
private const long DateTimeTicksPerMicrosecond = 10;
|
private const long DateTimeTicksPerMicrosecond = 10;
|
||||||
|
private const ulong MicrosecondsPerSecond = 1_000_000UL;
|
||||||
|
private const ulong MicrosecondsPerMinute = 60_000_000UL;
|
||||||
|
private const ulong MicrosecondsPerHour = 3_600_000_000UL;
|
||||||
|
private const ulong MicrosecondsPerDay = 86_400_000_000UL;
|
||||||
|
private const ulong MicrosecondsPerWeek = 604_800_000_000UL;
|
||||||
|
private const ulong UnixEpochTicks = 62_135_596_800_000_000UL;
|
||||||
|
private const ulong Win32FileTimeEpochTicks = 50_491_123_200_000_000UL;
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "lPEBYdVX0XQ",
|
||||||
|
ExportName = "sceRtcCheckValid",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcCheckValid(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (timeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return ValidateRtcDateTime(time);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "fNaZ4DbzHAE",
|
||||||
|
ExportName = "sceRtcCompareTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcCompareTick(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var tick1Address = ctx[CpuRegister.Rdi];
|
||||||
|
var tick2Address = ctx[CpuRegister.Rsi];
|
||||||
|
if (tick1Address == 0 || tick2Address == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(tick1Address, out var tick1) || !ctx.TryReadUInt64(tick2Address, out var tick2))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return tick1 < tick2 ? -1 : tick1 > tick2 ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8Yr143yEnRo",
|
||||||
|
ExportName = "sceRtcConvertLocalTimeToUtc",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcConvertLocalTimeToUtc(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var tickLocalAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var tickUtcAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (tickLocalAddress == 0 || tickUtcAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(tickLocalAddress, out var tickLocal))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryConvertTickToDateTime(tickLocal, out var localDateTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime utcDateTime;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
utcDateTime = TimeZoneInfo.ConvertTimeToUtc(localDateTime, TimeZoneInfo.Local);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteTickFromDateTime(ctx, tickUtcAddress, utcDateTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "M1TvFst-jrM",
|
||||||
|
ExportName = "sceRtcConvertUtcToLocalTime",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcConvertUtcToLocalTime(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var tickUtcAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var tickLocalAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (tickUtcAddress == 0 || tickLocalAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(tickUtcAddress, out var tickUtc))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryConvertTickToDateTime(tickUtc, out var utcDateTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime localDateTime;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
localDateTime = TimeZoneInfo.ConvertTimeFromUtc(utcDateTime, TimeZoneInfo.Local);
|
||||||
|
}
|
||||||
|
catch (ArgumentException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteTickFromDateTime(ctx, tickLocalAddress, localDateTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8SljQx6pDP8",
|
||||||
|
ExportName = "sceRtcEnd",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcEnd(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "LN3Zcb72Q0c",
|
||||||
|
ExportName = "sceRtcGetCurrentAdNetworkTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetCurrentAdNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "8lfvnRMqwEM",
|
||||||
|
ExportName = "sceRtcGetCurrentClock",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetCurrentClock(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (timeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeZoneMinutes = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
DateTimeOffset currentTime;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
currentTime = DateTimeOffset.UtcNow.ToOffset(TimeSpan.FromMinutes(timeZoneMinutes));
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteRtcDateTime(ctx, timeAddress, ToRtcDateTime(currentTime.DateTime)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ZPD1YOKI+Kw",
|
Nid = "ZPD1YOKI+Kw",
|
||||||
@@ -20,30 +203,38 @@ public static class RtcExports
|
|||||||
var timeAddress = ctx[CpuRegister.Rdi];
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
if (timeAddress == 0)
|
if (timeAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return unchecked((int)0x80B50002);
|
||||||
}
|
}
|
||||||
|
|
||||||
var now = DateTimeOffset.Now;
|
if (!TryWriteRtcDateTime(ctx, timeAddress, ToRtcDateTime(DateTimeOffset.Now.DateTime)))
|
||||||
Span<byte> rtcDateTime = stackalloc byte[16];
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)now.Year));
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)now.Month));
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)now.Day));
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)now.Hour));
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)now.Minute));
|
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)now.Second));
|
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
|
||||||
rtcDateTime[12..16],
|
|
||||||
checked((uint)((now.Ticks % TimeSpan.TicksPerSecond) / 10)));
|
|
||||||
|
|
||||||
if (!ctx.Memory.TryWrite(timeAddress, rtcDateTime))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ot1DE3gif84",
|
||||||
|
ExportName = "sceRtcGetCurrentDebugNetworkTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetCurrentDebugNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "zO9UL3qIINQ",
|
||||||
|
ExportName = "sceRtcGetCurrentNetworkTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetCurrentNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "HWxHOdbM-Pg",
|
||||||
|
ExportName = "sceRtcGetCurrentRawNetworkTick",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetCurrentRawNetworkTick(CpuContext ctx) => RtcGetCurrentTick(ctx);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "18B2NS1y9UU",
|
Nid = "18B2NS1y9UU",
|
||||||
ExportName = "sceRtcGetCurrentTick",
|
ExportName = "sceRtcGetCurrentTick",
|
||||||
@@ -54,7 +245,7 @@ public static class RtcExports
|
|||||||
var tickAddress = ctx[CpuRegister.Rdi];
|
var tickAddress = ctx[CpuRegister.Rdi];
|
||||||
if (tickAddress == 0)
|
if (tickAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return unchecked((int)0x80B50002);
|
||||||
}
|
}
|
||||||
|
|
||||||
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
|
var tickValue = unchecked((ulong)(DateTime.UtcNow.Ticks / DateTimeTicksPerMicrosecond));
|
||||||
@@ -63,7 +254,87 @@ public static class RtcExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "CyIK-i4XdgQ",
|
||||||
|
ExportName = "sceRtcGetDayOfWeek",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetDayOfWeek(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var year = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var month = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
var day = unchecked((int)ctx[CpuRegister.Rdx]);
|
||||||
|
if (!IsValidCalendarDate(year, month, day))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50004);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)new DateTime(year, month, day).DayOfWeek;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3O7Ln8AqJ1o",
|
||||||
|
ExportName = "sceRtcGetDaysInMonth",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetDaysInMonth(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var year = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var month = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
if (year < 1 || year > 9999)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50008);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (month < 1 || month > 12)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50009);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.DaysInMonth(year, month);
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "E7AR4o7Ny7E",
|
||||||
|
ExportName = "sceRtcGetDosTime",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetDosTime(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var dosTimeAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (timeAddress == 0 || dosTimeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validationResult = ValidateRtcDateTime(time);
|
||||||
|
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return validationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
uint dosTime = 0;
|
||||||
|
dosTime |= (uint)((time.Second / 2) & 0x1F);
|
||||||
|
dosTime |= (uint)(time.Minute & 0x3F) << 5;
|
||||||
|
dosTime |= (uint)(time.Hour & 0x1F) << 11;
|
||||||
|
dosTime |= (uint)(time.Day & 0x1F) << 16;
|
||||||
|
dosTime |= (uint)(time.Month & 0x0F) << 21;
|
||||||
|
dosTime |= (uint)((time.Year - 1980) & 0x7F) << 25;
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt32(dosTimeAddress, dosTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -74,45 +345,220 @@ public static class RtcExports
|
|||||||
LibraryName = "libSceRtc")]
|
LibraryName = "libSceRtc")]
|
||||||
public static int RtcGetTick(CpuContext ctx)
|
public static int RtcGetTick(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var dateTimeAddress = ctx[CpuRegister.Rdi];
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
var tickAddress = ctx[CpuRegister.Rsi];
|
var tickAddress = ctx[CpuRegister.Rsi];
|
||||||
if (dateTimeAddress == 0 || tickAddress == 0)
|
if (timeAddress == 0 || tickAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return unchecked((int)0x80B50002);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!TryReadRtcDateTime(ctx, dateTimeAddress, out var rtcDateTime))
|
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ulong tickValue;
|
var validationResult = ValidateRtcDateTime(time);
|
||||||
try
|
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
{
|
{
|
||||||
var baseDateTime = new DateTime(
|
return validationResult;
|
||||||
rtcDateTime.Year,
|
|
||||||
rtcDateTime.Month,
|
|
||||||
rtcDateTime.Day,
|
|
||||||
rtcDateTime.Hour,
|
|
||||||
rtcDateTime.Minute,
|
|
||||||
rtcDateTime.Second,
|
|
||||||
DateTimeKind.Utc);
|
|
||||||
tickValue = checked((ulong)((baseDateTime.Ticks / DateTimeTicksPerMicrosecond) + rtcDateTime.Microsecond));
|
|
||||||
}
|
}
|
||||||
catch (Exception ex) when (ex is ArgumentOutOfRangeException or OverflowException)
|
|
||||||
|
if (!TryConvertRtcDateTimeToTick(time, out var tick))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryWriteUInt64(tickAddress, tickValue))
|
if (!ctx.TryWriteUInt64(tickAddress, tick))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "jMNwqYr4R-k",
|
||||||
|
ExportName = "sceRtcGetTickResolution",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetTickResolution(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return (int)MicrosecondsPerSecond;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "BtqmpTRXHgk",
|
||||||
|
ExportName = "sceRtcGetTime_t",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetTimeT(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var timeTAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (timeAddress == 0 || timeTAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validationResult = ValidateRtcDateTime(time);
|
||||||
|
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return validationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryConvertRtcDateTimeToTick(time, out var tick))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var unixSeconds = tick < UnixEpochTicks ? 0UL : (tick - UnixEpochTicks) / MicrosecondsPerSecond;
|
||||||
|
if (!ctx.TryWriteUInt64(timeTAddress, unixSeconds))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "jfRO0uTjtzA",
|
||||||
|
ExportName = "sceRtcGetWin32FileTime",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcGetWin32FileTime(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var fileTimeAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (timeAddress == 0 || fileTimeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryReadRtcDateTime(ctx, timeAddress, out var time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var validationResult = ValidateRtcDateTime(time);
|
||||||
|
if (validationResult != (int)OrbisGen2Result.ORBIS_GEN2_OK)
|
||||||
|
{
|
||||||
|
return validationResult;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryConvertRtcDateTimeToTick(time, out var tick))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
||||||
|
}
|
||||||
|
|
||||||
|
var win32Time = tick < Win32FileTimeEpochTicks ? 0UL : (tick - Win32FileTimeEpochTicks) * 10UL;
|
||||||
|
if (!ctx.TryWriteUInt64(fileTimeAddress, win32Time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "LlodCMDbk3o",
|
||||||
|
ExportName = "sceRtcInit",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcInit(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Ug8pCwQvh0c",
|
||||||
|
ExportName = "sceRtcIsLeapYear",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcIsLeapYear(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var year = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
if (year < 1 || year > 9999)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50008);
|
||||||
|
}
|
||||||
|
|
||||||
|
return DateTime.IsLeapYear(year) ? 1 : 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "NR1J0N7L2xY",
|
||||||
|
ExportName = "sceRtcTickAddDays",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddDays(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerDay);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "MDc5cd8HfCA",
|
||||||
|
ExportName = "sceRtcTickAddHours",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddHours(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerHour);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "XPIiw58C+GM",
|
||||||
|
ExportName = "sceRtcTickAddMicroseconds",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddMicroseconds(CpuContext ctx) => AddTickDelta(ctx, 1UL);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "mn-tf4QiFzk",
|
||||||
|
ExportName = "sceRtcTickAddMinutes",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddMinutes(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerMinute);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "CL6y9q-XbuQ",
|
||||||
|
ExportName = "sceRtcTickAddMonths",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddMonths(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return AddCalendarDelta(ctx, dateTime => dateTime.AddMonths(unchecked((int)ctx[CpuRegister.Rdx])));
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "07O525HgICs",
|
||||||
|
ExportName = "sceRtcTickAddSeconds",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddSeconds(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerSecond);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "AqVMssr52Rc",
|
||||||
|
ExportName = "sceRtcTickAddTicks",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddTicks(CpuContext ctx) => AddTickDelta(ctx, 1UL);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "gI4t194c2W8",
|
||||||
|
ExportName = "sceRtcTickAddWeeks",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddWeeks(CpuContext ctx) => AddTickDelta(ctx, MicrosecondsPerWeek);
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "-5y2uJ62qS8",
|
||||||
|
ExportName = "sceRtcTickAddYears",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcTickAddYears(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return AddCalendarDelta(ctx, dateTime => dateTime.AddYears(unchecked((int)ctx[CpuRegister.Rdx])));
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "ueega6v3GUw",
|
Nid = "ueega6v3GUw",
|
||||||
ExportName = "sceRtcSetTick",
|
ExportName = "sceRtcSetTick",
|
||||||
@@ -120,11 +566,11 @@ public static class RtcExports
|
|||||||
LibraryName = "libSceRtc")]
|
LibraryName = "libSceRtc")]
|
||||||
public static int RtcSetTick(CpuContext ctx)
|
public static int RtcSetTick(CpuContext ctx)
|
||||||
{
|
{
|
||||||
var dateTimeAddress = ctx[CpuRegister.Rdi];
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
var tickAddress = ctx[CpuRegister.Rsi];
|
var tickAddress = ctx[CpuRegister.Rsi];
|
||||||
if (dateTimeAddress == 0 || tickAddress == 0)
|
if (timeAddress == 0 || tickAddress == 0)
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return unchecked((int)0x80B50002);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (!ctx.TryReadUInt64(tickAddress, out var tickValue))
|
if (!ctx.TryReadUInt64(tickAddress, out var tickValue))
|
||||||
@@ -132,40 +578,249 @@ public static class RtcExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (tickValue > long.MaxValue / DateTimeTicksPerMicrosecond)
|
if (!TryConvertTickToRtcDateTime(tickValue, out var time))
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
return unchecked((int)0x80B50004);
|
||||||
}
|
}
|
||||||
|
|
||||||
DateTime dateTime;
|
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||||
try
|
|
||||||
{
|
|
||||||
dateTime = new DateTime(checked((long)tickValue * DateTimeTicksPerMicrosecond), DateTimeKind.Utc);
|
|
||||||
}
|
|
||||||
catch (ArgumentOutOfRangeException)
|
|
||||||
{
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!TryWriteRtcDateTime(ctx, dateTimeAddress, dateTime))
|
|
||||||
{
|
{
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx[CpuRegister.Rax] = 0;
|
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryReadRtcDateTime(CpuContext ctx, ulong address, out RtcDateTime rtcDateTime)
|
[SysAbiExport(
|
||||||
|
Nid = "aYPCd1cChyg",
|
||||||
|
ExportName = "sceRtcSetDosTime",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcSetDosTime(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (timeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
var dosTime = unchecked((uint)ctx[CpuRegister.Rsi]);
|
||||||
|
var time = new RtcDateTime(
|
||||||
|
(ushort)(1980 + ((dosTime >> 25) & 0x7F)),
|
||||||
|
(ushort)((dosTime >> 21) & 0x0F),
|
||||||
|
(ushort)((dosTime >> 16) & 0x1F),
|
||||||
|
(ushort)((dosTime >> 11) & 0x1F),
|
||||||
|
(ushort)((dosTime >> 5) & 0x3F),
|
||||||
|
(ushort)((dosTime << 1) & 0x3E),
|
||||||
|
0);
|
||||||
|
|
||||||
|
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "bDEVVP4bTjQ",
|
||||||
|
ExportName = "sceRtcSetTime_t",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcSetTimeT(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (timeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
var timeSeconds = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||||
|
if (timeSeconds < 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tick = UnixEpochTicks + unchecked((ulong)timeSeconds) * MicrosecondsPerSecond;
|
||||||
|
if (!TryConvertTickToRtcDateTime(tick, out var time))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "n5JiAJXsbcs",
|
||||||
|
ExportName = "sceRtcSetWin32FileTime",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceRtc")]
|
||||||
|
public static int RtcSetWin32FileTime(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var timeAddress = ctx[CpuRegister.Rdi];
|
||||||
|
if (timeAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileTime = unchecked((long)ctx[CpuRegister.Rsi]);
|
||||||
|
if (fileTime < 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
var tick = Win32FileTimeEpochTicks + unchecked((ulong)fileTime / 10UL);
|
||||||
|
if (!TryConvertTickToRtcDateTime(tick, out var time))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteRtcDateTime(ctx, timeAddress, time))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int AddTickDelta(CpuContext ctx, ulong microsecondsPerUnit)
|
||||||
|
{
|
||||||
|
var destinationAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||||
|
var delta = unchecked((long)ctx[CpuRegister.Rdx]);
|
||||||
|
if (destinationAddress == 0 || sourceAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(sourceAddress, out var sourceTick))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var resultTick = checked((long)sourceTick + checked(delta * (long)microsecondsPerUnit));
|
||||||
|
if (resultTick < 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryWriteUInt64(destinationAddress, unchecked((ulong)resultTick)))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int AddCalendarDelta(CpuContext ctx, Func<DateTime, DateTime> transform)
|
||||||
|
{
|
||||||
|
var destinationAddress = ctx[CpuRegister.Rdi];
|
||||||
|
var sourceAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (destinationAddress == 0 || sourceAddress == 0)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50002);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!ctx.TryReadUInt64(sourceAddress, out var sourceTick))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryConvertTickToDateTime(sourceTick, out var sourceDateTime))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
DateTime resultDateTime;
|
||||||
|
try
|
||||||
|
{
|
||||||
|
resultDateTime = transform(sourceDateTime);
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50003);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!TryWriteTickFromDateTime(ctx, destinationAddress, resultDateTime))
|
||||||
|
{
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ValidateRtcDateTime(RtcDateTime time)
|
||||||
|
{
|
||||||
|
if (time.Year < 1 || time.Year > 9999)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50008);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Month < 1 || time.Month > 12)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B50009);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Day < 1 || time.Day > DateTime.DaysInMonth(time.Year, time.Month))
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B5000A);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Hour > 23)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B5000B);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Minute > 59)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B5000C);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Second > 59)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B5000D);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (time.Microsecond > 999_999)
|
||||||
|
{
|
||||||
|
return unchecked((int)0x80B5000E);
|
||||||
|
}
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool IsValidCalendarDate(int year, int month, int day)
|
||||||
|
{
|
||||||
|
if (year < 1 || year > 9999 || month < 1 || month > 12)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
return day >= 1 && day <= DateTime.DaysInMonth(year, month);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryReadRtcDateTime(CpuContext ctx, ulong address, out RtcDateTime time)
|
||||||
{
|
{
|
||||||
Span<byte> buffer = stackalloc byte[16];
|
Span<byte> buffer = stackalloc byte[16];
|
||||||
if (!ctx.Memory.TryRead(address, buffer))
|
if (!ctx.Memory.TryRead(address, buffer))
|
||||||
{
|
{
|
||||||
rtcDateTime = default;
|
time = default;
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
rtcDateTime = new RtcDateTime(
|
time = new RtcDateTime(
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(buffer[0..2]),
|
BinaryPrimitives.ReadUInt16LittleEndian(buffer[0..2]),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..4]),
|
BinaryPrimitives.ReadUInt16LittleEndian(buffer[2..4]),
|
||||||
BinaryPrimitives.ReadUInt16LittleEndian(buffer[4..6]),
|
BinaryPrimitives.ReadUInt16LittleEndian(buffer[4..6]),
|
||||||
@@ -176,19 +831,117 @@ public static class RtcExports
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, DateTime dateTime)
|
private static bool TryWriteRtcDateTime(CpuContext ctx, ulong address, RtcDateTime dateTime)
|
||||||
{
|
{
|
||||||
Span<byte> rtcDateTime = stackalloc byte[16];
|
Span<byte> buffer = stackalloc byte[16];
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[0..2], checked((ushort)dateTime.Year));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[0..2], dateTime.Year);
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[2..4], checked((ushort)dateTime.Month));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[2..4], dateTime.Month);
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[4..6], checked((ushort)dateTime.Day));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[4..6], dateTime.Day);
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[6..8], checked((ushort)dateTime.Hour));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[6..8], dateTime.Hour);
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[8..10], checked((ushort)dateTime.Minute));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[8..10], dateTime.Minute);
|
||||||
BinaryPrimitives.WriteUInt16LittleEndian(rtcDateTime[10..12], checked((ushort)dateTime.Second));
|
BinaryPrimitives.WriteUInt16LittleEndian(buffer[10..12], dateTime.Second);
|
||||||
BinaryPrimitives.WriteUInt32LittleEndian(
|
BinaryPrimitives.WriteUInt32LittleEndian(buffer[12..16], dateTime.Microsecond);
|
||||||
rtcDateTime[12..16],
|
return ctx.Memory.TryWrite(address, buffer);
|
||||||
checked((uint)((dateTime.Ticks % TimeSpan.TicksPerSecond) / DateTimeTicksPerMicrosecond)));
|
}
|
||||||
return ctx.Memory.TryWrite(address, rtcDateTime);
|
|
||||||
|
private static RtcDateTime ToRtcDateTime(DateTime dateTime)
|
||||||
|
{
|
||||||
|
return new RtcDateTime(
|
||||||
|
(ushort)dateTime.Year,
|
||||||
|
(ushort)dateTime.Month,
|
||||||
|
(ushort)dateTime.Day,
|
||||||
|
(ushort)dateTime.Hour,
|
||||||
|
(ushort)dateTime.Minute,
|
||||||
|
(ushort)dateTime.Second,
|
||||||
|
(uint)(dateTime.Ticks % TimeSpan.TicksPerSecond / DateTimeTicksPerMicrosecond));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static RtcDateTime ToRtcDateTime(DateTimeOffset dateTimeOffset)
|
||||||
|
{
|
||||||
|
return ToRtcDateTime(dateTimeOffset.DateTime);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryWriteTickFromDateTime(CpuContext ctx, ulong address, DateTime dateTime)
|
||||||
|
{
|
||||||
|
return ctx.TryWriteUInt64(address, unchecked((ulong)(dateTime.Ticks / DateTimeTicksPerMicrosecond)));
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryConvertTickToDateTime(ulong tick, out DateTime dateTime)
|
||||||
|
{
|
||||||
|
var maxSupportedTick = unchecked((ulong)(DateTime.MaxValue.Ticks / DateTimeTicksPerMicrosecond));
|
||||||
|
if (tick > maxSupportedTick)
|
||||||
|
{
|
||||||
|
dateTime = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
dateTime = new DateTime(checked((long)(tick * (ulong)DateTimeTicksPerMicrosecond)), DateTimeKind.Utc);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
dateTime = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryConvertRtcDateTimeToTick(RtcDateTime time, out ulong tick)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var year = time.Year;
|
||||||
|
var month = time.Month;
|
||||||
|
var day = time.Day;
|
||||||
|
if (month > 2)
|
||||||
|
{
|
||||||
|
month -= 3;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
month += 9;
|
||||||
|
year -= 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
var century = year / 100;
|
||||||
|
var yearOfCentury = year - (100 * century);
|
||||||
|
|
||||||
|
ulong days = ((146097UL * (ulong)century) >> 2)
|
||||||
|
+ ((1461UL * (ulong)yearOfCentury) >> 2)
|
||||||
|
+ ((153UL * (ulong)month + 2UL) / 5UL)
|
||||||
|
+ day;
|
||||||
|
days -= 307UL;
|
||||||
|
days *= MicrosecondsPerDay;
|
||||||
|
|
||||||
|
var timeOfDay = (ulong)time.Hour * MicrosecondsPerHour
|
||||||
|
+ (ulong)time.Minute * MicrosecondsPerMinute
|
||||||
|
+ (ulong)time.Second * MicrosecondsPerSecond
|
||||||
|
+ time.Microsecond;
|
||||||
|
|
||||||
|
tick = days + timeOfDay;
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (OverflowException)
|
||||||
|
{
|
||||||
|
tick = 0;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static bool TryConvertTickToRtcDateTime(ulong tick, out RtcDateTime time)
|
||||||
|
{
|
||||||
|
try
|
||||||
|
{
|
||||||
|
var dateTime = new DateTime(checked((long)(tick * DateTimeTicksPerMicrosecond)), DateTimeKind.Utc);
|
||||||
|
time = ToRtcDateTime(dateTime);
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
catch (ArgumentOutOfRangeException)
|
||||||
|
{
|
||||||
|
time = default;
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private readonly record struct RtcDateTime(
|
private readonly record struct RtcDateTime(
|
||||||
|
|||||||
@@ -72,4 +72,26 @@ public static class SystemGestureExports
|
|||||||
ctx[CpuRegister.Rax] = 0;
|
ctx[CpuRegister.Rax] = 0;
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "1MMK0W-kMgA",
|
||||||
|
ExportName = "sceSystemGestureAppendTouchRecognizer",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemGesture")]
|
||||||
|
public static int SystemGestureAppendTouchRecognizer(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "wPJGwI2RM2I",
|
||||||
|
ExportName = "sceSystemGestureUpdateAllTouchRecognizer",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemGesture")]
|
||||||
|
public static int SystemGestureUpdateAllTouchRecognizer(CpuContext ctx)
|
||||||
|
{
|
||||||
|
ctx[CpuRegister.Rax] = 0;
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -96,4 +96,11 @@ public static class SystemServiceExports
|
|||||||
VulkanVideoPresenter.HideSplashScreen();
|
VulkanVideoPresenter.HideSplashScreen();
|
||||||
return ctx.SetReturn(0);
|
return ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "3s8cHiCBKBE",
|
||||||
|
ExportName = "sceSystemServiceReportAbnormalTermination",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceSystemService")]
|
||||||
|
public static int SystemServiceReportAbnormalTermination(CpuContext ctx) => ctx.SetReturn(0);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using SharpEmu.HLE;
|
||||||
|
|
||||||
|
namespace SharpEmu.Libs.Ult;
|
||||||
|
|
||||||
|
public static class UltExports
|
||||||
|
{
|
||||||
|
// Games initialize the ULT (user-level thread) runtime before spinning up
|
||||||
|
// their job systems; an unresolved import here (0x80020002) makes engines
|
||||||
|
// treat the whole task system as unavailable. The emulator schedules guest
|
||||||
|
// pthreads natively, so accepting the initialization is sufficient.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "hZIg1EWGsHM",
|
||||||
|
ExportName = "sceUltInitialize",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceUlt")]
|
||||||
|
public static int UltInitialize(CpuContext ctx)
|
||||||
|
{
|
||||||
|
return ctx.SetReturn(0, typeof(long));
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -18,6 +18,9 @@ public static class UserServiceExports
|
|||||||
private const string PrimaryUserName = "SharpEmu";
|
private const string PrimaryUserName = "SharpEmu";
|
||||||
private static int _loginEventDelivered;
|
private static int _loginEventDelivered;
|
||||||
|
|
||||||
|
private static readonly bool _traceUserService =
|
||||||
|
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USER_SERVICE"), "1", StringComparison.Ordinal);
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "j3YMu1MVNNo",
|
Nid = "j3YMu1MVNNo",
|
||||||
ExportName = "sceUserServiceInitialize",
|
ExportName = "sceUserServiceInitialize",
|
||||||
@@ -42,9 +45,11 @@ public static class UserServiceExports
|
|||||||
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
return ctx.SetReturn(OrbisUserServiceErrorInvalidArgument);
|
||||||
}
|
}
|
||||||
|
|
||||||
return ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
|
var result = ctx.TryWriteInt32(userIdAddress, PrimaryUserId)
|
||||||
? ctx.SetReturn(0)
|
? 0
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
TraceUserService($"get_initial_user out=0x{userIdAddress:X16} value={PrimaryUserId} result=0x{result:X8}");
|
||||||
|
return ctx.SetReturn(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -124,9 +129,12 @@ public static class UserServiceExports
|
|||||||
|
|
||||||
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
Span<byte> output = stackalloc byte[nameBytes.Length + 1];
|
||||||
nameBytes.CopyTo(output);
|
nameBytes.CopyTo(output);
|
||||||
return ctx.Memory.TryWrite(nameAddress, output)
|
var result = ctx.Memory.TryWrite(nameAddress, output)
|
||||||
? ctx.SetReturn(0)
|
? 0
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
|
TraceUserService(
|
||||||
|
$"get_user_name user={userId} out=0x{nameAddress:X16} capacity=0x{capacity:X} value='{PrimaryUserName}' result=0x{result:X8}");
|
||||||
|
return ctx.SetReturn(result);
|
||||||
}
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
@@ -152,4 +160,12 @@ public static class UserServiceExports
|
|||||||
? ctx.SetReturn(0)
|
? ctx.SetReturn(0)
|
||||||
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
: ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void TraceUserService(string message)
|
||||||
|
{
|
||||||
|
if (_traceUserService)
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] user_service.{message}");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -38,12 +38,66 @@ public static class VideoOutExports
|
|||||||
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
|
private const ulong SceVideoOutPixelFormatA2R10G10B10 = 0x88060000;
|
||||||
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
|
private const ulong SceVideoOutPixelFormatA2R10G10B10Srgb = 0x88000000;
|
||||||
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
|
private const ulong SceVideoOutPixelFormatA2R10G10B10Bt2020Pq = 0x88740000;
|
||||||
|
private const ulong SceVideoOutInternalEventVblank = 0x5;
|
||||||
private const ulong SceVideoOutInternalEventFlip = 0x6;
|
private const ulong SceVideoOutInternalEventFlip = 0x6;
|
||||||
private const short OrbisKernelEventFilterVideoOut = -13;
|
private const short OrbisKernelEventFilterVideoOut = -13;
|
||||||
|
|
||||||
private static readonly object _stateGate = new();
|
private static readonly object _stateGate = new();
|
||||||
private static readonly object _frameDumpGate = new();
|
private static readonly object _frameDumpGate = new();
|
||||||
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
|
private static readonly Dictionary<int, VideoOutPortState> _ports = new();
|
||||||
|
|
||||||
|
// Hardware raises vblank autonomously; UE blocks its frame loop on it.
|
||||||
|
private static Timer? _vblankPumpTimer;
|
||||||
|
private static int _vblankPumpActive;
|
||||||
|
private const int VblankPumpIntervalMs = 16;
|
||||||
|
|
||||||
|
private static void EnsureVblankPumpStarted()
|
||||||
|
{
|
||||||
|
if (_vblankPumpTimer is not null)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
_vblankPumpTimer = new Timer(
|
||||||
|
static _ => PumpVblanks(),
|
||||||
|
null,
|
||||||
|
VblankPumpIntervalMs,
|
||||||
|
VblankPumpIntervalMs);
|
||||||
|
}
|
||||||
|
|
||||||
|
private static void PumpVblanks()
|
||||||
|
{
|
||||||
|
if (Interlocked.Exchange(ref _vblankPumpActive, 1) != 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
try
|
||||||
|
{
|
||||||
|
VideoOutPortState[] ports;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
if (_ports.Count == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Signalling reaches WakeBlockedThreads -> Pump(), which serialises on one global
|
||||||
|
// flag. Waking an unwatched queue would hold it 60x/sec and starve guest threads.
|
||||||
|
ports = _ports.Values.Where(static port => port.VblankEvents.Count != 0).ToArray();
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var port in ports)
|
||||||
|
{
|
||||||
|
SignalVblank(port);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
finally
|
||||||
|
{
|
||||||
|
Volatile.Write(ref _vblankPumpActive, 0);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static readonly Dictionary<(int Handle, int BufferIndex, ulong Address), ulong> _lastFrameFingerprints = new();
|
private static readonly Dictionary<(int Handle, int BufferIndex, ulong Address), ulong> _lastFrameFingerprints = new();
|
||||||
private static int _nextHandle = 1;
|
private static int _nextHandle = 1;
|
||||||
private static int _frameDumpCount;
|
private static int _frameDumpCount;
|
||||||
@@ -53,11 +107,17 @@ public static class VideoOutExports
|
|||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_FPS"),
|
||||||
"1",
|
"1",
|
||||||
StringComparison.Ordinal);
|
StringComparison.Ordinal);
|
||||||
|
private static readonly bool _logVideoOutSync = string.Equals(
|
||||||
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VIDEOOUT_SYNC"),
|
||||||
|
"1",
|
||||||
|
StringComparison.Ordinal);
|
||||||
private static long _frameRateWindowStart = Stopwatch.GetTimestamp();
|
private static long _frameRateWindowStart = Stopwatch.GetTimestamp();
|
||||||
private static long _submittedFrameCount;
|
private static long _submittedFrameCount;
|
||||||
private static long _presentedFrameCount;
|
private static long _presentedFrameCount;
|
||||||
|
private static long _vblankSignalCount;
|
||||||
|
private static long _flipSubmitCount;
|
||||||
|
|
||||||
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version)
|
public static void ConfigureApplicationInfo(string? title, string? titleId, string? version, string? emulatorCommitSha)
|
||||||
{
|
{
|
||||||
var parts = new List<string>();
|
var parts = new List<string>();
|
||||||
if (!string.IsNullOrWhiteSpace(title))
|
if (!string.IsNullOrWhiteSpace(title))
|
||||||
@@ -72,9 +132,10 @@ public static class VideoOutExports
|
|||||||
|
|
||||||
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
|
var application = parts.Count == 0 ? "VideoOut" : string.Join(' ', parts);
|
||||||
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
|
var versionSuffix = string.IsNullOrWhiteSpace(version) ? string.Empty : $" v{version.Trim()}";
|
||||||
|
var commitSuffix = string.IsNullOrWhiteSpace(emulatorCommitSha) ? string.Empty : $" \u00b7 {emulatorCommitSha.Trim()}";
|
||||||
lock (_stateGate)
|
lock (_stateGate)
|
||||||
{
|
{
|
||||||
_windowTitle = $"SharpEmu - {application}{versionSuffix}";
|
_windowTitle = $"SharpEmu{commitSuffix} - {application}{versionSuffix}";
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -99,6 +160,7 @@ public static class VideoOutExports
|
|||||||
public float Gamma { get; set; } = 1.0f;
|
public float Gamma { get; set; } = 1.0f;
|
||||||
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
|
public VideoOutBufferGroup?[] Groups { get; } = new VideoOutBufferGroup?[MaxDisplayBufferGroups];
|
||||||
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
|
public VideoOutBufferSlot[] BufferSlots { get; } = CreateBufferSlots();
|
||||||
|
public List<FlipEventRegistration> VblankEvents { get; } = new();
|
||||||
public List<FlipEventRegistration> FlipEvents { get; } = new();
|
public List<FlipEventRegistration> FlipEvents { get; } = new();
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -168,6 +230,7 @@ public static class VideoOutExports
|
|||||||
{
|
{
|
||||||
Handle = handle,
|
Handle = handle,
|
||||||
};
|
};
|
||||||
|
EnsureVblankPumpStarted();
|
||||||
return handle;
|
return handle;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -312,11 +375,48 @@ public static class VideoOutExports
|
|||||||
}
|
}
|
||||||
|
|
||||||
Thread.Sleep(1);
|
Thread.Sleep(1);
|
||||||
lock (_stateGate)
|
SignalVblank(port);
|
||||||
|
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "Xru92wHJRmg",
|
||||||
|
ExportName = "sceVideoOutAddVblankEvent",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutAddVblankEvent(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var equeue = ctx[CpuRegister.Rdi];
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rsi]);
|
||||||
|
if (!TryGetPort(handle, out var port))
|
||||||
{
|
{
|
||||||
port.VblankCount++;
|
return OrbisVideoOutErrorInvalidHandle;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (!KernelEventQueueCompatExports.IsValidEqueue(equeue))
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidEventQueue;
|
||||||
|
}
|
||||||
|
|
||||||
|
var userData = ctx[CpuRegister.Rdx];
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
var existingIndex = port.VblankEvents.FindIndex(registration => registration.Equeue == equeue);
|
||||||
|
if (existingIndex >= 0)
|
||||||
|
{
|
||||||
|
port.VblankEvents[existingIndex] = new FlipEventRegistration(equeue, userData);
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
port.VblankEvents.Add(new FlipEventRegistration(equeue, userData));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Some engines wait on this queue before issuing their first flip. Provide a first
|
||||||
|
// edge now; later calls to WaitVblank advance the same notification sequence.
|
||||||
|
SignalVblank(port);
|
||||||
|
TraceVideoOut($"videoout.add_vblank_event eq=0x{equeue:X16} handle={handle} udata=0x{userData:X16}");
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -371,6 +471,53 @@ public static class VideoOutExports
|
|||||||
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg, submitGpuImage: true);
|
return SubmitFlip(ctx, handle, bufferIndex, flipMode, flipArg, submitGpuImage: true);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Struct layout matches the classic SceVideoOutFlipStatus (40 bytes):
|
||||||
|
// count, processTime, tsc, flipArg, currentBuffer, flipPendingNum.
|
||||||
|
[SysAbiExport(
|
||||||
|
Nid = "SbU3dwp80lQ",
|
||||||
|
ExportName = "sceVideoOutGetFlipStatus",
|
||||||
|
Target = Generation.Gen4 | Generation.Gen5,
|
||||||
|
LibraryName = "libSceVideoOut")]
|
||||||
|
public static int VideoOutGetFlipStatus(CpuContext ctx)
|
||||||
|
{
|
||||||
|
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
|
||||||
|
var statusAddress = ctx[CpuRegister.Rsi];
|
||||||
|
if (statusAddress == 0)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidAddress;
|
||||||
|
}
|
||||||
|
|
||||||
|
VideoOutPortState? port;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
_ports.TryGetValue(handle, out port);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (port is null)
|
||||||
|
{
|
||||||
|
return OrbisVideoOutErrorInvalidHandle;
|
||||||
|
}
|
||||||
|
|
||||||
|
ulong count;
|
||||||
|
long flipArg;
|
||||||
|
uint currentBuffer;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
count = port.FlipCount;
|
||||||
|
flipArg = 0;
|
||||||
|
currentBuffer = unchecked((uint)port.CurrentBuffer);
|
||||||
|
}
|
||||||
|
|
||||||
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x00, count);
|
||||||
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x08, 0);
|
||||||
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x10, 0);
|
||||||
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x18, unchecked((ulong)flipArg));
|
||||||
|
KernelMemoryCompatExports.TryWriteUInt64Compat(ctx, statusAddress + 0x20, currentBuffer);
|
||||||
|
|
||||||
|
TraceVideoOut($"videoout.get_flip_status handle={handle} count={count} currentBuffer={currentBuffer}");
|
||||||
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
|
}
|
||||||
|
|
||||||
[SysAbiExport(
|
[SysAbiExport(
|
||||||
Nid = "zgXifHT9ErY",
|
Nid = "zgXifHT9ErY",
|
||||||
ExportName = "sceVideoOutIsFlipPending",
|
ExportName = "sceVideoOutIsFlipPending",
|
||||||
@@ -407,7 +554,8 @@ public static class VideoOutExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
|
if (filter != OrbisKernelEventFilterVideoOut ||
|
||||||
|
ident is not (SceVideoOutInternalEventVblank or SceVideoOutInternalEventFlip))
|
||||||
{
|
{
|
||||||
return OrbisVideoOutErrorInvalidEvent;
|
return OrbisVideoOutErrorInvalidEvent;
|
||||||
}
|
}
|
||||||
@@ -436,7 +584,8 @@ public static class VideoOutExports
|
|||||||
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
|
||||||
}
|
}
|
||||||
|
|
||||||
if (filter != OrbisKernelEventFilterVideoOut || ident != SceVideoOutInternalEventFlip)
|
if (filter != OrbisKernelEventFilterVideoOut ||
|
||||||
|
ident is not (SceVideoOutInternalEventVblank or SceVideoOutInternalEventFlip))
|
||||||
{
|
{
|
||||||
return OrbisVideoOutErrorInvalidEvent;
|
return OrbisVideoOutErrorInvalidEvent;
|
||||||
}
|
}
|
||||||
@@ -756,6 +905,38 @@ public static class VideoOutExports
|
|||||||
return groupIndex < 0 ? groupIndex : setIndex;
|
return groupIndex < 0 ? groupIndex : setIndex;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private static void SignalVblank(VideoOutPortState port)
|
||||||
|
{
|
||||||
|
List<FlipEventRegistration> vblankEvents;
|
||||||
|
ulong eventHint;
|
||||||
|
lock (_stateGate)
|
||||||
|
{
|
||||||
|
port.VblankCount++;
|
||||||
|
eventHint = SceVideoOutInternalEventVblank |
|
||||||
|
((port.VblankCount & 0x0000_FFFF_FFFF_FFFFUL) << 16);
|
||||||
|
vblankEvents = new List<FlipEventRegistration>(port.VblankEvents);
|
||||||
|
}
|
||||||
|
|
||||||
|
var signalCount = Interlocked.Increment(ref _vblankSignalCount);
|
||||||
|
|
||||||
|
foreach (var vblankEvent in vblankEvents)
|
||||||
|
{
|
||||||
|
_ = KernelEventQueueCompatExports.TriggerDisplayEvent(
|
||||||
|
vblankEvent.Equeue,
|
||||||
|
SceVideoOutInternalEventVblank,
|
||||||
|
OrbisKernelEventFilterVideoOut,
|
||||||
|
eventHint,
|
||||||
|
vblankEvent.UserData);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_logVideoOutSync && (signalCount <= 8 || signalCount % 60 == 0))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][SYNC] vblank#{signalCount} handle={port.Handle} count={port.VblankCount} " +
|
||||||
|
$"queues={vblankEvents.Count} hint=0x{eventHint:X16}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static int SubmitFlip(
|
private static int SubmitFlip(
|
||||||
CpuContext ctx,
|
CpuContext ctx,
|
||||||
int handle,
|
int handle,
|
||||||
@@ -790,11 +971,14 @@ public static class VideoOutExports
|
|||||||
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
|
flipEvents = new List<FlipEventRegistration>(port.FlipEvents);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var guestImageSubmitted = false;
|
||||||
|
ulong guestImageAddress = 0;
|
||||||
if (submitGpuImage &&
|
if (submitGpuImage &&
|
||||||
bufferIndex >= 0 &&
|
bufferIndex >= 0 &&
|
||||||
TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer))
|
TryGetDisplayBufferInfo(handle, bufferIndex, out var displayBuffer))
|
||||||
{
|
{
|
||||||
_ = VulkanVideoPresenter.TrySubmitGuestImage(
|
guestImageAddress = displayBuffer.Address;
|
||||||
|
guestImageSubmitted = VulkanVideoPresenter.TrySubmitGuestImage(
|
||||||
displayBuffer.Address,
|
displayBuffer.Address,
|
||||||
displayBuffer.Width,
|
displayBuffer.Width,
|
||||||
displayBuffer.Height,
|
displayBuffer.Height,
|
||||||
@@ -819,6 +1003,15 @@ public static class VideoOutExports
|
|||||||
flipEvent.UserData);
|
flipEvent.UserData);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var flipCount = Interlocked.Increment(ref _flipSubmitCount);
|
||||||
|
if (_logVideoOutSync && (flipCount <= 8 || flipCount % 60 == 0))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][SYNC] flip#{flipCount} handle={handle} buffer={bufferIndex} " +
|
||||||
|
$"addr=0x{guestImageAddress:X16} submitted={guestImageSubmitted} " +
|
||||||
|
$"flipQueues={flipEvents.Count}");
|
||||||
|
}
|
||||||
|
|
||||||
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
TraceVideoOut($"videoout.submit_flip handle={handle} index={bufferIndex} mode={flipMode} arg={flipArg} events={flipEvents.Count}");
|
||||||
ReportFrameRate(presented: false);
|
ReportFrameRate(presented: false);
|
||||||
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
|
||||||
@@ -902,6 +1095,16 @@ public static class VideoOutExports
|
|||||||
TraceVideoOut(
|
TraceVideoOut(
|
||||||
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
$"videoout.register_buffers handle={port.Handle} group={groupIndex} start={startIndex} count={addresses.Length} fmt=0x{attribute.PixelFormat:X} tile={attribute.TilingMode} {attribute.Width}x{attribute.Height} pitch={attribute.PitchInPixel}");
|
||||||
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
|
VulkanVideoPresenter.EnsureStarted(attribute.Width, attribute.Height);
|
||||||
|
|
||||||
|
var guestFormat = MapPixelFormatToGuestTextureFormat(attribute.PixelFormat);
|
||||||
|
if (guestFormat != 0)
|
||||||
|
{
|
||||||
|
foreach (var address in addresses)
|
||||||
|
{
|
||||||
|
VulkanVideoPresenter.RegisterKnownDisplayBuffer(address, guestFormat);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
return groupIndex;
|
return groupIndex;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1123,6 +1326,22 @@ public static class VideoOutExports
|
|||||||
? 4u
|
? 4u
|
||||||
: 0u;
|
: 0u;
|
||||||
|
|
||||||
|
// Maps the PS5 VideoOut pixel format space to the AGC "guest texture format" tags
|
||||||
|
// VulkanVideoPresenter._availableGuestImages keys on (see VulkanVideoPresenter.
|
||||||
|
// GetGuestTextureFormat: format=10 => 56 for 8-bit RGBA variants, format=9 => 9 for 10-bit).
|
||||||
|
private static uint MapPixelFormatToGuestTextureFormat(ulong pixelFormat) =>
|
||||||
|
NormalizePixelFormat(pixelFormat) switch
|
||||||
|
{
|
||||||
|
SceVideoOutPixelFormatA8R8G8B8Srgb or
|
||||||
|
SceVideoOutPixelFormatA8B8G8R8Srgb or
|
||||||
|
SceVideoOutPixelFormatB8G8R8A8Unorm or
|
||||||
|
SceVideoOutPixelFormatR8G8B8A8Unorm => 56u,
|
||||||
|
SceVideoOutPixelFormatA2R10G10B10 or
|
||||||
|
SceVideoOutPixelFormatA2R10G10B10Srgb or
|
||||||
|
SceVideoOutPixelFormatA2R10G10B10Bt2020Pq => 9u,
|
||||||
|
_ => 0u,
|
||||||
|
};
|
||||||
|
|
||||||
private static ulong NormalizePixelFormat(ulong pixelFormat)
|
private static ulong NormalizePixelFormat(ulong pixelFormat)
|
||||||
{
|
{
|
||||||
if (GetBytesPerPixel(pixelFormat) != 0)
|
if (GetBytesPerPixel(pixelFormat) != 0)
|
||||||
|
|||||||
@@ -178,10 +178,18 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
private static uint _windowHeight;
|
private static uint _windowHeight;
|
||||||
private static bool _closed;
|
private static bool _closed;
|
||||||
private const string DebugUtilsExtensionName = "VK_EXT_debug_utils";
|
private const string DebugUtilsExtensionName = "VK_EXT_debug_utils";
|
||||||
|
private const uint NvidiaVendorId = 0x10DE;
|
||||||
private static bool _splashHidden;
|
private static bool _splashHidden;
|
||||||
private static long _enqueuedGuestWorkSequence;
|
private static long _enqueuedGuestWorkSequence;
|
||||||
private static long _completedGuestWorkSequence;
|
private static long _completedGuestWorkSequence;
|
||||||
|
|
||||||
|
private static bool ShouldTracePresentedGuestImageContentsForDiagnostics()
|
||||||
|
{
|
||||||
|
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
|
||||||
|
return string.Equals(mode, "1", StringComparison.Ordinal) ||
|
||||||
|
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
|
||||||
|
}
|
||||||
|
|
||||||
public static void EnsureStarted(uint width, uint height)
|
public static void EnsureStarted(uint width, uint height)
|
||||||
{
|
{
|
||||||
if (width == 0 || height == 0)
|
if (width == 0 || height == 0)
|
||||||
@@ -279,6 +287,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=Submit {width}x{height}");
|
||||||
|
}
|
||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (_closed)
|
if (_closed)
|
||||||
@@ -319,6 +332,11 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine($"[LOADER][TRACE] vk.submit_call kind=SubmitGuestDraw({drawKind}) {width}x{height}");
|
||||||
|
}
|
||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (_closed ||
|
if (_closed ||
|
||||||
@@ -376,6 +394,12 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] vk.submit_call kind=SubmitTranslatedDraw {width}x{height} textures={textures.Count}");
|
||||||
|
}
|
||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (_closed)
|
if (_closed)
|
||||||
@@ -442,6 +466,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] vk.submit_call kind=SubmitOffscreenTranslatedDraw " +
|
||||||
|
$"target=0x{target.Address:X16} {target.Width}x{target.Height} textures={textures.Count}");
|
||||||
|
}
|
||||||
|
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (_closed)
|
if (_closed)
|
||||||
@@ -569,9 +600,31 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
var traceSubmission = false;
|
var traceSubmission = false;
|
||||||
lock (_gate)
|
lock (_gate)
|
||||||
{
|
{
|
||||||
if (_closed ||
|
var known = _availableGuestImages.ContainsKey(address);
|
||||||
!_availableGuestImages.ContainsKey(address))
|
if (ShouldTracePresentedGuestImageContentsForDiagnostics())
|
||||||
{
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] vk.submit_call kind=TrySubmitGuestImage addr=0x{address:X16} " +
|
||||||
|
$"{width}x{height} known={known}");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (_closed)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// The caller (VideoOutExports.SubmitFlip) reports the flip as successful either
|
||||||
|
// way, so an unregistered address means the frame is dropped silently; warn once
|
||||||
|
// per address so that shows up in the log.
|
||||||
|
if (!known)
|
||||||
|
{
|
||||||
|
if (_tracedGuestImageSubmissions.Add((address, width, height)))
|
||||||
|
{
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][WARN] vk.submit_guest_image_unknown addr=0x{address:X16} " +
|
||||||
|
$"{width}x{height} - flip target was never registered as a render output");
|
||||||
|
}
|
||||||
|
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -588,6 +641,19 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
RequiredGuestWorkSequence: 0,
|
RequiredGuestWorkSequence: 0,
|
||||||
IsSplash: false,
|
IsSplash: false,
|
||||||
GuestImageAddress: address);
|
GuestImageAddress: address);
|
||||||
|
if (_thread is not null)
|
||||||
|
{
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
_windowWidth = width;
|
||||||
|
_windowHeight = height;
|
||||||
|
_thread = new Thread(Run)
|
||||||
|
{
|
||||||
|
IsBackground = true,
|
||||||
|
Name = "SharpEmu Vulkan VideoOut",
|
||||||
|
};
|
||||||
|
_thread.Start();
|
||||||
}
|
}
|
||||||
|
|
||||||
if (traceSubmission)
|
if (traceSubmission)
|
||||||
@@ -601,6 +667,21 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Display buffers registered through sceVideoOutRegisterBuffers are valid flip targets
|
||||||
|
// even when no AGC render-target write to them was ever observed.
|
||||||
|
internal static void RegisterKnownDisplayBuffer(ulong address, uint guestFormat)
|
||||||
|
{
|
||||||
|
if (address == 0 || guestFormat == 0)
|
||||||
|
{
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
lock (_gate)
|
||||||
|
{
|
||||||
|
_availableGuestImages[address] = guestFormat;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
internal static bool IsGpuGuestImageAvailable(
|
internal static bool IsGpuGuestImageAvailable(
|
||||||
ulong address,
|
ulong address,
|
||||||
uint format,
|
uint format,
|
||||||
@@ -1422,6 +1503,13 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, devicePointer), "vkEnumeratePhysicalDevices");
|
Check(_vk.EnumeratePhysicalDevices(_instance, &deviceCount, devicePointer), "vkEnumeratePhysicalDevices");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Hybrid laptops enumerate the iGPU first, and AMD's integrated driver
|
||||||
|
// segfaults compiling some translated shaders, so rank rather than take
|
||||||
|
// the first hit. SHARPEMU_VK_DEVICE=<substring> pins an adapter by name.
|
||||||
|
var deviceOverride = Environment.GetEnvironmentVariable("SHARPEMU_VK_DEVICE");
|
||||||
|
var bestScore = int.MinValue;
|
||||||
|
var found = false;
|
||||||
|
|
||||||
foreach (var device in devices)
|
foreach (var device in devices)
|
||||||
{
|
{
|
||||||
uint queueCount = 0;
|
uint queueCount = 0;
|
||||||
@@ -1441,13 +1529,60 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
|
|
||||||
_physicalDevice = device;
|
_vk.GetPhysicalDeviceProperties(device, out var properties);
|
||||||
_queueFamilyIndex = index;
|
var name = SilkMarshal.PtrToString((nint)properties.DeviceName) ?? string.Empty;
|
||||||
return;
|
var score = ScorePhysicalDevice(properties, name, deviceOverride);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Vulkan candidate: {name} ({properties.DeviceType}) score={score}");
|
||||||
|
|
||||||
|
if (score > bestScore)
|
||||||
|
{
|
||||||
|
bestScore = score;
|
||||||
|
_physicalDevice = device;
|
||||||
|
_queueFamilyIndex = index;
|
||||||
|
found = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
throw new InvalidOperationException("No Vulkan graphics/present queue was found.");
|
if (!found)
|
||||||
|
{
|
||||||
|
throw new InvalidOperationException("No Vulkan graphics/present queue was found.");
|
||||||
|
}
|
||||||
|
|
||||||
|
_vk.GetPhysicalDeviceProperties(_physicalDevice, out var selected);
|
||||||
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][INFO] Vulkan device: {SilkMarshal.PtrToString((nint)selected.DeviceName)} ({selected.DeviceType})");
|
||||||
|
}
|
||||||
|
|
||||||
|
private static int ScorePhysicalDevice(
|
||||||
|
PhysicalDeviceProperties properties,
|
||||||
|
string name,
|
||||||
|
string? deviceOverride)
|
||||||
|
{
|
||||||
|
if (!string.IsNullOrWhiteSpace(deviceOverride))
|
||||||
|
{
|
||||||
|
return name.Contains(deviceOverride, StringComparison.OrdinalIgnoreCase) ? 1000 : -1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
var score = properties.DeviceType switch
|
||||||
|
{
|
||||||
|
PhysicalDeviceType.DiscreteGpu => 300,
|
||||||
|
PhysicalDeviceType.VirtualGpu => 100,
|
||||||
|
PhysicalDeviceType.Cpu => 50,
|
||||||
|
// Last resort: only picked when nothing else can present.
|
||||||
|
PhysicalDeviceType.IntegratedGpu => -100,
|
||||||
|
_ => 10,
|
||||||
|
};
|
||||||
|
|
||||||
|
if (properties.VendorID == NvidiaVendorId)
|
||||||
|
{
|
||||||
|
score += 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
return score;
|
||||||
}
|
}
|
||||||
|
|
||||||
private void CreateDevice()
|
private void CreateDevice()
|
||||||
@@ -5033,14 +5168,19 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
_renderPass,
|
_renderPass,
|
||||||
_extent);
|
_extent);
|
||||||
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
|
if (ShouldTracePresentedGuestImageContentsForDiagnostics() &&
|
||||||
!_firstGuestDrawPresented &&
|
!_firstGuestDrawPresented)
|
||||||
translatedResources.Textures is
|
|
||||||
[
|
|
||||||
{ GuestImage: { } guestImage },
|
|
||||||
] &&
|
|
||||||
_tracedGuestImageContents.Add(guestImage.Address))
|
|
||||||
{
|
{
|
||||||
TraceGuestImageContents(guestImage);
|
Console.Error.WriteLine(
|
||||||
|
$"[LOADER][TRACE] vk.translated_draw kind={presentation.DrawKind} " +
|
||||||
|
$"textures={translatedResources.Textures.Length}");
|
||||||
|
foreach (var boundTexture in translatedResources.Textures)
|
||||||
|
{
|
||||||
|
if (boundTexture.GuestImage is { } guestImage &&
|
||||||
|
_tracedGuestImageContents.Add(guestImage.Address))
|
||||||
|
{
|
||||||
|
TraceGuestImageContents(guestImage);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
catch (Exception exception)
|
catch (Exception exception)
|
||||||
@@ -5810,13 +5950,6 @@ internal static unsafe class VulkanVideoPresenter
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static bool ShouldTracePresentedGuestImageContentsForDiagnostics()
|
|
||||||
{
|
|
||||||
var mode = Environment.GetEnvironmentVariable("SHARPEMU_TRACE_GUEST_IMAGES");
|
|
||||||
return string.Equals(mode, "1", StringComparison.Ordinal) ||
|
|
||||||
string.Equals(mode, "present", StringComparison.OrdinalIgnoreCase);
|
|
||||||
}
|
|
||||||
|
|
||||||
private static bool ShouldTraceVulkanResources() =>
|
private static bool ShouldTraceVulkanResources() =>
|
||||||
string.Equals(
|
string.Equals(
|
||||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VK_RESOURCES"),
|
Environment.GetEnvironmentVariable("SHARPEMU_LOG_VK_RESOURCES"),
|
||||||
|
|||||||
@@ -98,6 +98,12 @@
|
|||||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||||
},
|
},
|
||||||
"sharpemu.hle": {
|
"sharpemu.hle": {
|
||||||
|
"type": "Project",
|
||||||
|
"dependencies": {
|
||||||
|
"SharpEmu.Logging": "[1.0.0, )"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"sharpemu.logging": {
|
||||||
"type": "Project"
|
"type": "Project"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,144 @@
|
|||||||
|
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||||
|
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||||
|
|
||||||
|
using System.Reflection;
|
||||||
|
|
||||||
|
namespace SharpEmu.Logging;
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Build provenance for the running emulator, populated at compile time from
|
||||||
|
/// GitHub Actions environment variables via <c>[AssemblyMetadata]</c> and
|
||||||
|
/// surfaced as a touchHLE-style banner at the top of the log.
|
||||||
|
/// </summary>
|
||||||
|
public static class BuildInfo
|
||||||
|
{
|
||||||
|
private const string ProjectUrl = "https://github.com/par274/sharpemu";
|
||||||
|
private const string CanonicalRepository = "par274/sharpemu";
|
||||||
|
|
||||||
|
/// <summary>Short commit hash the build was produced from, or <c>null</c>.</summary>
|
||||||
|
public static string? CommitSha { get; }
|
||||||
|
|
||||||
|
/// <summary>Branch the build was produced from, or <c>null</c>.</summary>
|
||||||
|
public static string? Branch { get; }
|
||||||
|
|
||||||
|
/// <summary>Repository slug (<c>owner/name</c>) the build came from, or <c>null</c>.</summary>
|
||||||
|
public static string? Repository { get; }
|
||||||
|
|
||||||
|
/// <summary>URL of the GitHub Actions workflow run that produced the build, or <c>null</c>.</summary>
|
||||||
|
public static string? WorkflowRunUrl { get; }
|
||||||
|
|
||||||
|
/// <summary>Build configuration (e.g. <c>Debug</c> or <c>Release</c>), or <c>null</c>.</summary>
|
||||||
|
public static string? Configuration { get; }
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Whether this build is an official release: a Release-configuration CI build
|
||||||
|
/// from the canonical repository, produced by a push to <c>main</c> or a manual
|
||||||
|
/// workflow dispatch (matching the CI <c>release</c> job). All other builds
|
||||||
|
/// (pull requests, forks, feature branches, and local/Debug builds) are
|
||||||
|
/// considered unofficial.
|
||||||
|
/// </summary>
|
||||||
|
public static bool IsOfficialRelease { get; }
|
||||||
|
|
||||||
|
static BuildInfo()
|
||||||
|
{
|
||||||
|
var metadata = ReadMetadata();
|
||||||
|
|
||||||
|
CommitSha = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildSha"));
|
||||||
|
if (CommitSha is { Length: > 7 })
|
||||||
|
{
|
||||||
|
CommitSha = CommitSha[..7];
|
||||||
|
}
|
||||||
|
|
||||||
|
Branch = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildBranch"));
|
||||||
|
Repository = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRepository"));
|
||||||
|
Configuration = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildConfiguration"));
|
||||||
|
|
||||||
|
var serverUrl = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildServerUrl"));
|
||||||
|
var runId = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRunId"));
|
||||||
|
if (serverUrl is not null && Repository is not null && runId is not null)
|
||||||
|
{
|
||||||
|
WorkflowRunUrl = $"{serverUrl.TrimEnd('/')}/{Repository}/actions/runs/{runId}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var eventName = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildEventName"));
|
||||||
|
var gitRef = Normalize(metadata.GetValueOrDefault("SharpEmu.BuildRef"));
|
||||||
|
|
||||||
|
var isReleaseConfig = string.Equals(Configuration, "Release", StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isCanonicalRepo = string.Equals(Repository, CanonicalRepository, StringComparison.OrdinalIgnoreCase);
|
||||||
|
var isReleaseTrigger =
|
||||||
|
string.Equals(eventName, "workflow_dispatch", StringComparison.OrdinalIgnoreCase) ||
|
||||||
|
(string.Equals(eventName, "push", StringComparison.OrdinalIgnoreCase) &&
|
||||||
|
string.Equals(gitRef, "refs/heads/main", StringComparison.OrdinalIgnoreCase));
|
||||||
|
|
||||||
|
IsOfficialRelease = isReleaseConfig && isCanonicalRepo && isReleaseTrigger && CommitSha is not null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// The multi-line banner, e.g.
|
||||||
|
/// <code>
|
||||||
|
/// SharpEmu UNOFFICIAL f11ac59 — https://github.com/par274/sharpemu
|
||||||
|
///
|
||||||
|
/// Built from branch "main" of "par274/sharpemu" by GitHub Actions workflow run https://github.com/par274/sharpemu/actions/runs/123.
|
||||||
|
/// </code>
|
||||||
|
/// Official release builds drop the <c>UNOFFICIAL</c> tag. Falls back to a
|
||||||
|
/// local-build line when no CI provenance is present.
|
||||||
|
/// </summary>
|
||||||
|
public static string Banner
|
||||||
|
{
|
||||||
|
get
|
||||||
|
{
|
||||||
|
string version;
|
||||||
|
if (CommitSha is null)
|
||||||
|
{
|
||||||
|
version = "UNOFFICIAL";
|
||||||
|
}
|
||||||
|
else if (IsOfficialRelease)
|
||||||
|
{
|
||||||
|
version = CommitSha;
|
||||||
|
}
|
||||||
|
else
|
||||||
|
{
|
||||||
|
version = $"UNOFFICIAL {CommitSha}";
|
||||||
|
}
|
||||||
|
|
||||||
|
var header = $"SharpEmu {version} — {ProjectUrl}";
|
||||||
|
|
||||||
|
if (Branch is null || Repository is null || WorkflowRunUrl is null)
|
||||||
|
{
|
||||||
|
return $"{header}{Environment.NewLine}{Environment.NewLine}Local build (not produced by CI).";
|
||||||
|
}
|
||||||
|
|
||||||
|
return $"{header}{Environment.NewLine}{Environment.NewLine}" +
|
||||||
|
$"Built from branch \"{Branch}\" of \"{Repository}\" by GitHub Actions workflow run {WorkflowRunUrl}.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private static Dictionary<string, string> ReadMetadata()
|
||||||
|
{
|
||||||
|
var result = new Dictionary<string, string>(StringComparer.Ordinal);
|
||||||
|
var assemblies = new[] { Assembly.GetEntryAssembly(), typeof(BuildInfo).Assembly };
|
||||||
|
|
||||||
|
foreach (var assembly in assemblies)
|
||||||
|
{
|
||||||
|
if (assembly is null)
|
||||||
|
{
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach (var attribute in assembly.GetCustomAttributes<AssemblyMetadataAttribute>())
|
||||||
|
{
|
||||||
|
if (attribute.Key.StartsWith("SharpEmu.Build", StringComparison.Ordinal) &&
|
||||||
|
!result.ContainsKey(attribute.Key) &&
|
||||||
|
attribute.Value is not null)
|
||||||
|
{
|
||||||
|
result[attribute.Key] = attribute.Value;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
private static string? Normalize(string? value) =>
|
||||||
|
string.IsNullOrWhiteSpace(value) ? null : value.Trim();
|
||||||
|
}
|
||||||
@@ -12,8 +12,14 @@ public static class SharpEmuLog
|
|||||||
new(StringComparer.Ordinal);
|
new(StringComparer.Ordinal);
|
||||||
private static readonly object ConfigurationSync = new();
|
private static readonly object ConfigurationSync = new();
|
||||||
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
|
private static volatile LogLevel _minimumLevel = ResolveMinimumLevelFromEnvironment();
|
||||||
|
private static bool _fileCapturesAllLevels;
|
||||||
private static ISharpEmuLogSink _sink = ResolveSinkFromEnvironment();
|
private static ISharpEmuLogSink _sink = ResolveSinkFromEnvironment();
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Entries below this level are dropped. When a SHARPEMU_LOG_FILE sink is
|
||||||
|
/// active it only limits the console — the file receives every level.
|
||||||
|
/// <see cref="LogLevel.None"/> disables logging entirely, file included.
|
||||||
|
/// </summary>
|
||||||
public static LogLevel MinimumLevel
|
public static LogLevel MinimumLevel
|
||||||
{
|
{
|
||||||
get => _minimumLevel;
|
get => _minimumLevel;
|
||||||
@@ -40,6 +46,11 @@ public static class SharpEmuLog
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// A replacement sink is not the environment-configured
|
||||||
|
// console+file pair, so the minimum level applies globally
|
||||||
|
// again.
|
||||||
|
_fileCapturesAllLevels = false;
|
||||||
|
|
||||||
if (_sink is IDisposable disposable)
|
if (_sink is IDisposable disposable)
|
||||||
{
|
{
|
||||||
try
|
try
|
||||||
@@ -122,7 +133,14 @@ public static class SharpEmuLog
|
|||||||
internal static bool IsEnabled(LogLevel level)
|
internal static bool IsEnabled(LogLevel level)
|
||||||
{
|
{
|
||||||
var minimum = _minimumLevel;
|
var minimum = _minimumLevel;
|
||||||
return minimum != LogLevel.None && level >= minimum;
|
if (minimum == LogLevel.None)
|
||||||
|
{
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// With a file sink capturing all levels, the console filter is
|
||||||
|
// applied per-sink instead of here.
|
||||||
|
return _fileCapturesAllLevels || level >= minimum;
|
||||||
}
|
}
|
||||||
|
|
||||||
internal static void Write(
|
internal static void Write(
|
||||||
@@ -187,7 +205,10 @@ public static class SharpEmuLog
|
|||||||
try
|
try
|
||||||
{
|
{
|
||||||
var fileSink = new FileLogSink(logFilePath, append: true, includeTimestamp: true);
|
var fileSink = new FileLogSink(logFilePath, append: true, includeTimestamp: true);
|
||||||
return new CompositeLogSink(consoleSink, fileSink);
|
// The file gets every level; the configured minimum only
|
||||||
|
// limits what reaches the console.
|
||||||
|
_fileCapturesAllLevels = true;
|
||||||
|
return new CompositeLogSink(new MinimumLevelFilterSink(consoleSink), fileSink);
|
||||||
}
|
}
|
||||||
catch (Exception ex)
|
catch (Exception ex)
|
||||||
{
|
{
|
||||||
@@ -199,6 +220,25 @@ public static class SharpEmuLog
|
|||||||
return consoleSink;
|
return consoleSink;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// <summary>
|
||||||
|
/// Forwards only entries at or above <see cref="MinimumLevel"/>. Wraps
|
||||||
|
/// the console sink when a file sink captures all levels.
|
||||||
|
/// </summary>
|
||||||
|
private sealed class MinimumLevelFilterSink : ISharpEmuLogSink
|
||||||
|
{
|
||||||
|
private readonly ISharpEmuLogSink _inner;
|
||||||
|
|
||||||
|
internal MinimumLevelFilterSink(ISharpEmuLogSink inner) => _inner = inner;
|
||||||
|
|
||||||
|
public void Write(in LogEntry entry)
|
||||||
|
{
|
||||||
|
if (entry.Level >= _minimumLevel)
|
||||||
|
{
|
||||||
|
_inner.Write(in entry);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
private static bool IsTrueLike(string? text)
|
private static bool IsTrueLike(string? text)
|
||||||
{
|
{
|
||||||
if (string.IsNullOrWhiteSpace(text))
|
if (string.IsNullOrWhiteSpace(text))
|
||||||
|
|||||||
Reference in New Issue
Block a user