mirror of
https://github.com/par274/sharpemu.git
synced 2026-07-25 20:28:48 +08:00
Compare commits
114 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| d2f3511002 | |||
| 90a5d5176f | |||
| 28a43e09c7 | |||
| 093cfa1f3e | |||
| d8397b022e | |||
| 85cc2b9892 | |||
| 293194c40b | |||
| 1f09de8896 | |||
| ddc452b4fc | |||
| 61a97baf85 | |||
| e80f96ecf5 | |||
| d49c0f1f10 | |||
| 1d33ef90fc | |||
| 26a570633c | |||
| e4f89445b9 | |||
| 1254cc1564 | |||
| 503b3f4d6b | |||
| 6b37ab54f2 | |||
| a84d2344fb | |||
| 787d3a1efb | |||
| 884584da67 | |||
| d43edc865a | |||
| cf6964710a | |||
| 4f028d0483 | |||
| 4db98bd8fe | |||
| 4600a2ed1f | |||
| c5c5ee1f36 | |||
| b48b1a5e09 | |||
| 511a01e03a | |||
| aaebfe017b | |||
| 9cdc8550ec | |||
| d6fccedab8 | |||
| fed7a6d062 | |||
| 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 | |||
| 48a694e509 | |||
| 165927882b | |||
| d9d1aeaef9 | |||
| 57e737b5d7 | |||
| a78b02f22b | |||
| 70ec2928ea | |||
| c618c116ba | |||
| 29021b5a71 | |||
| c0fd6a80e8 | |||
| 7337683c16 | |||
| b36ecc121c | |||
| 42f0fa0e83 | |||
| 41d61bde41 | |||
| ac96fe5292 | |||
| 9b9ca8f707 | |||
| db20387502 | |||
| 44c43c8ebd | |||
| 4b5b27e686 | |||
| 5c823d0e8b | |||
| 1679897089 | |||
| ce8f75411a | |||
| f75da92fa5 | |||
| 9eaaac7cee | |||
| d446779d34 | |||
| 585d11b93b | |||
| 17f9535f0e | |||
| 8bfbb9c7fe | |||
| 2b8fe71a82 | |||
| 29b7f086c9 | |||
| eb3d8d6eb1 | |||
| 787c75b9b3 | |||
| 2a784aa405 | |||
| 2649857cb5 | |||
| c4b4aed2c5 | |||
| 757f6ea4b5 | |||
| df3ee02f8d | |||
| dfe61ec643 | |||
| 52e17b5056 | |||
| c1f3abfd5d | |||
| b5ee2c2cb7 | |||
| 16c1b74636 |
+1
-1
@@ -14,7 +14,7 @@ indent_size = 4
|
||||
indent_style = space
|
||||
tab_width = 4
|
||||
|
||||
[*.{md,json,yml,props,csproj}]
|
||||
[*.{md,json,yml,xml,props,csproj}]
|
||||
|
||||
indent_size = 2
|
||||
tab_width = 2
|
||||
|
||||
@@ -16,6 +16,11 @@ body:
|
||||
Example title:
|
||||
Demon’s Souls (PPSA01341)
|
||||
|
||||
**Before reporting:**
|
||||
- Test the game on a **fresh build from `main`**, not an old release.
|
||||
- Copy the **exact commit SHA** shown at the top of the log.
|
||||
- `DEBUG` and `TRACE` lines are diagnostic context, not errors by themselves; report the last milestone or the first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop.
|
||||
|
||||
- type: input
|
||||
id: game_name
|
||||
attributes:
|
||||
@@ -73,11 +78,20 @@ body:
|
||||
id: logs
|
||||
attributes:
|
||||
label: Log File
|
||||
description: Drag and drop your log file here, or click to browse
|
||||
description: Attach the **full** log file (`.log`, `.txt`, or `.zip`). Do not paste only a fragment — the maintainer needs the complete log from launch to crash.
|
||||
validations:
|
||||
required: false
|
||||
required: true
|
||||
accept: ".log,.txt,.zip"
|
||||
|
||||
- type: input
|
||||
id: last_milestone_first_error
|
||||
attributes:
|
||||
label: Last Milestone / First Error
|
||||
description: Paste the **last useful milestone line** or the **first `WARNING`, `ERROR`, or `CRITICAL` line connected to the stop** from the log. `DEBUG`/`TRACE` lines do not count unless a real error follows them.
|
||||
placeholder: "e.g. CpuEngine: native-only OR [ERROR] Native backend FAILED: ..."
|
||||
validations:
|
||||
required: true
|
||||
|
||||
- type: dropdown
|
||||
id: operating_system
|
||||
attributes:
|
||||
@@ -119,7 +133,8 @@ body:
|
||||
id: emulator_version
|
||||
attributes:
|
||||
label: Emulator Version / Commit
|
||||
placeholder: e.g. v0.0.1 / a1b2c3d
|
||||
description: Copy the **exact short SHA** from the top of the log. Do not enter just `0.0.1` without a commit hash.
|
||||
placeholder: e.g. a1b2c3d
|
||||
validations:
|
||||
required: true
|
||||
|
||||
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 101 KiB |
Binary file not shown.
|
Before Width: | Height: | Size: 89 KiB |
@@ -126,7 +126,7 @@ jobs:
|
||||
needs:
|
||||
- init
|
||||
- build
|
||||
if: github.event_name == 'push' || github.event_name == 'workflow_dispatch'
|
||||
if: github.event_name == 'workflow_dispatch' || (github.event_name == 'push' && github.ref == 'refs/heads/main')
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
|
||||
@@ -26,6 +26,7 @@ arm64/
|
||||
*.userosscache
|
||||
*.sln.docstates
|
||||
|
||||
.packages
|
||||
packages/
|
||||
*.nupkg
|
||||
.nuget/
|
||||
@@ -36,3 +37,6 @@ packages/
|
||||
Thumbs.db
|
||||
Desktop.ini
|
||||
ehthumbs.db
|
||||
|
||||
.vs/
|
||||
.idea/
|
||||
|
||||
@@ -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>
|
||||
<DebugSymbols>false</DebugSymbols>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Build provenance surfaced by SharpEmu.Logging.BuildInfo as a banner at the
|
||||
top of the log. Populated from GitHub Actions environment variables when
|
||||
building in CI; empty locally. -->
|
||||
<ItemGroup>
|
||||
<AssemblyMetadata Include="SharpEmu.BuildConfiguration" Value="$(Configuration)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_SHA)' != ''" Include="SharpEmu.BuildSha" Value="$(GITHUB_SHA)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_REF_NAME)' != ''" Include="SharpEmu.BuildBranch" Value="$(GITHUB_REF_NAME)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_REF)' != ''" Include="SharpEmu.BuildRef" Value="$(GITHUB_REF)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_EVENT_NAME)' != ''" Include="SharpEmu.BuildEventName" Value="$(GITHUB_EVENT_NAME)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_REPOSITORY)' != ''" Include="SharpEmu.BuildRepository" Value="$(GITHUB_REPOSITORY)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_SERVER_URL)' != ''" Include="SharpEmu.BuildServerUrl" Value="$(GITHUB_SERVER_URL)" />
|
||||
<AssemblyMetadata Condition="'$(GITHUB_RUN_ID)' != ''" Include="SharpEmu.BuildRunId" Value="$(GITHUB_RUN_ID)" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
|
||||
@@ -1,16 +1,22 @@
|
||||
<!--
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project>
|
||||
<PropertyGroup>
|
||||
<ManagePackageVersionsCentrally>true</ManagePackageVersionsCentrally>
|
||||
</PropertyGroup>
|
||||
<ItemGroup>
|
||||
<PackageVersion Include="Avalonia" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Desktop" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Fonts.Inter" Version="11.3.18" />
|
||||
<PackageVersion Include="Avalonia.Themes.Fluent" Version="11.3.18" />
|
||||
<PackageVersion Include="Iced" Version="1.21.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
|
||||
<PackageVersion Include="Silk.NET.Windowing" Version="2.23.0" />
|
||||
<!-- Transitive of Avalonia.Desktop; pinned to fix GHSA-xrw6-gwf8-vvr9 -->
|
||||
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.21.3" />
|
||||
</ItemGroup>
|
||||
</Project>
|
||||
</Project>
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Alex Barney
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
+10
-1
@@ -3,13 +3,22 @@ version = 1
|
||||
[[annotations]]
|
||||
path = [
|
||||
"REUSE.toml",
|
||||
"nuget.config",
|
||||
"global.json",
|
||||
"**/packages.lock.json",
|
||||
"scripts/ps5_names.txt",
|
||||
"src/SharpEmu.HLE/Aerolib/aerolib.bin",
|
||||
"src/SharpEmu.GUI/Languages/**",
|
||||
"_logs/**",
|
||||
".github/images/**"
|
||||
".github/images/**",
|
||||
"assets/images/**"
|
||||
]
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "SharpEmu Emulator Project"
|
||||
SPDX-License-Identifier = "GPL-2.0-or-later"
|
||||
|
||||
[[annotations]]
|
||||
path = "src/SharpEmu.GUI/Atrac9/**"
|
||||
precedence = "aggregate"
|
||||
SPDX-FileCopyrightText = "2018 Alex Barney"
|
||||
SPDX-License-Identifier = "MIT"
|
||||
|
||||
@@ -7,6 +7,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<Folder Name="/src/">
|
||||
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
|
||||
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
|
||||
<Project Path="src/SharpEmu.GUI/SharpEmu.GUI.csproj" />
|
||||
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
|
||||
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
|
||||
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
|
||||
|
||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 92 KiB |
|
Before Width: | Height: | Size: 1.1 MiB After Width: | Height: | Size: 1.1 MiB |
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
|
||||
<configuration>
|
||||
<config>
|
||||
<add key="globalPackagesFolder" value=".packages" />
|
||||
</config>
|
||||
|
||||
<packageSources>
|
||||
<clear />
|
||||
<add key="Nuget" value="https://api.nuget.org/v3/index.json" />
|
||||
</packageSources>
|
||||
|
||||
<packageSourceMapping>
|
||||
<packageSource key="Nuget">
|
||||
<package pattern="*" />
|
||||
</packageSource>
|
||||
</packageSourceMapping>
|
||||
</configuration>
|
||||
@@ -3,8 +3,8 @@
|
||||
|
||||
#!/usr/bin/env python3
|
||||
|
||||
import struct
|
||||
import hashlib
|
||||
import struct
|
||||
from base64 import b64encode as base64enc
|
||||
from binascii import unhexlify as uhx
|
||||
from pathlib import Path
|
||||
@@ -21,7 +21,7 @@ def name2nid(name):
|
||||
def generate():
|
||||
names_path = Path(NAMES)
|
||||
output_path = Path(OUTPUT)
|
||||
|
||||
|
||||
entries = []
|
||||
with open(names_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
@@ -29,12 +29,12 @@ def generate():
|
||||
if name:
|
||||
nid = name2nid(name)
|
||||
entries.append((nid, name))
|
||||
|
||||
|
||||
print(f"Found {len(entries)} entries")
|
||||
|
||||
data = bytearray()
|
||||
data.extend(struct.pack('<I', len(entries)))
|
||||
|
||||
|
||||
for nid, name in entries:
|
||||
nid_bytes = nid.encode('utf-8')
|
||||
name_bytes = name.encode('utf-8')
|
||||
@@ -42,10 +42,11 @@ def generate():
|
||||
data.extend(nid_bytes)
|
||||
data.extend(struct.pack('<H', len(name_bytes)))
|
||||
data.extend(name_bytes)
|
||||
|
||||
|
||||
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with open(output_path, 'wb') as f:
|
||||
f.write(data)
|
||||
|
||||
|
||||
print(f"Generated: {output_path} ({len(data):,} bytes)")
|
||||
print(f"Total entries: {len(entries)}")
|
||||
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
+533
-15
@@ -3,16 +3,20 @@
|
||||
|
||||
using SharpEmu.Core.Runtime;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.GUI;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.CLI;
|
||||
|
||||
internal static partial class Program
|
||||
{
|
||||
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 string MitigatedChildFlag = "--sharpemu-mitigated-child";
|
||||
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
|
||||
@@ -20,32 +24,82 @@ internal static partial class Program
|
||||
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
|
||||
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
||||
private const int JobObjectExtendedLimitInformation = 9;
|
||||
private const 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_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
private const int ATTACH_PARENT_PROCESS = -1;
|
||||
private const int STD_INPUT_HANDLE = -10;
|
||||
private const int STD_OUTPUT_HANDLE = -11;
|
||||
private const int STD_ERROR_HANDLE = -12;
|
||||
private const uint GENERIC_READ = 0x80000000;
|
||||
private const uint GENERIC_WRITE = 0x40000000;
|
||||
private const uint FILE_SHARE_READ = 0x00000001;
|
||||
private const uint FILE_SHARE_WRITE = 0x00000002;
|
||||
private const uint OPEN_EXISTING = 3;
|
||||
|
||||
[STAThread]
|
||||
private static int Main(string[] args)
|
||||
{
|
||||
try
|
||||
{
|
||||
return Run(args);
|
||||
}
|
||||
finally
|
||||
{
|
||||
DropConsoleFileMirror();
|
||||
SharpEmuLog.Shutdown();
|
||||
}
|
||||
}
|
||||
|
||||
private static int Run(string[] args)
|
||||
{
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
if (args.Length == 0 && !isMitigatedChild)
|
||||
{
|
||||
// No arguments: open the desktop frontend. Any argument selects
|
||||
// the classic CLI behavior below.
|
||||
return GuiLauncher.Run();
|
||||
}
|
||||
|
||||
// The executable uses the GUI subsystem, so CLI mode has to connect
|
||||
// itself to a console before the first write.
|
||||
EnsureCliConsole();
|
||||
UseUtf8ConsoleOutput();
|
||||
if (isMitigatedChild && TryGetLogFileArgument(args, out var earlyLogFilePath))
|
||||
{
|
||||
TryEnableConsoleFileMirror(earlyLogFilePath);
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[DEBUG] SharpEmu starting with {args.Length} args");
|
||||
|
||||
args = NormalizeInternalArguments(args, out var isMitigatedChild);
|
||||
if (!isMitigatedChild && TryRunMitigatedChild(args, out var childExitCode))
|
||||
{
|
||||
return childExitCode;
|
||||
}
|
||||
|
||||
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();
|
||||
return 1;
|
||||
}
|
||||
|
||||
if (!isMitigatedChild && !string.IsNullOrWhiteSpace(logFilePath))
|
||||
{
|
||||
TryEnableConsoleFileMirror(logFilePath);
|
||||
}
|
||||
|
||||
SharpEmuLog.MinimumLevel = logLevel;
|
||||
|
||||
Log.Info(BuildInfo.Banner);
|
||||
Log.Info(HostSystemInfo.Summary);
|
||||
|
||||
ebootPath = Path.GetFullPath(ebootPath);
|
||||
Console.Error.WriteLine($"[DEBUG] Full path: {ebootPath}");
|
||||
|
||||
|
||||
if (!File.Exists(ebootPath))
|
||||
{
|
||||
Log.Error($"EBOOT file was not found: {ebootPath}");
|
||||
@@ -101,9 +155,107 @@ internal static partial class Program
|
||||
return result == OrbisGen2Result.ORBIS_GEN2_OK ? 0 : 4;
|
||||
}
|
||||
|
||||
private static void EnsureCliConsole()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Standard handles already provided (pipes or file redirection, e.g.
|
||||
// when the GUI or a script launches us): use them as-is.
|
||||
if (IsHandleValid(GetStdHandle(STD_OUTPUT_HANDLE)) && IsHandleValid(GetStdHandle(STD_ERROR_HANDLE)))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer the console of the parent process (interactive terminal);
|
||||
// create one only when started with arguments but no terminal at all
|
||||
// (e.g. a shortcut), so usage and errors remain visible.
|
||||
if (!AttachConsole(ATTACH_PARENT_PROCESS) && GetConsoleWindow() == 0)
|
||||
{
|
||||
_ = AllocConsole();
|
||||
}
|
||||
|
||||
RebindStdHandleToConsole(STD_OUTPUT_HANDLE);
|
||||
RebindStdHandleToConsole(STD_ERROR_HANDLE);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Makes console writes UTF-8 so the GUI's pipe reader (and any modern
|
||||
/// terminal) decodes non-ASCII text correctly. Without this, redirected
|
||||
/// output falls back to the OS ANSI code page and characters like "—"
|
||||
/// arrive mangled.
|
||||
/// </summary>
|
||||
private static void UseUtf8ConsoleOutput()
|
||||
{
|
||||
try
|
||||
{
|
||||
// Also recreates the redirected Console.Out/Error writers with
|
||||
// the new encoding.
|
||||
Console.OutputEncoding = Encoding.UTF8;
|
||||
return;
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// No attached console (GUI-subsystem child with piped output):
|
||||
// wrap the raw handles instead.
|
||||
}
|
||||
|
||||
if (Console.IsOutputRedirected)
|
||||
{
|
||||
Console.SetOut(new StreamWriter(
|
||||
Console.OpenStandardOutput(),
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
|
||||
{
|
||||
AutoFlush = true,
|
||||
});
|
||||
}
|
||||
|
||||
if (Console.IsErrorRedirected)
|
||||
{
|
||||
Console.SetError(new StreamWriter(
|
||||
Console.OpenStandardError(),
|
||||
new UTF8Encoding(encoderShouldEmitUTF8Identifier: false))
|
||||
{
|
||||
AutoFlush = true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private static void RebindStdHandleToConsole(int stdHandle)
|
||||
{
|
||||
if (IsHandleValid(GetStdHandle(stdHandle)) || GetConsoleWindow() == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
var conOut = CreateFileW(
|
||||
"CONOUT$",
|
||||
GENERIC_READ | GENERIC_WRITE,
|
||||
FILE_SHARE_READ | FILE_SHARE_WRITE,
|
||||
0,
|
||||
OPEN_EXISTING,
|
||||
0,
|
||||
0);
|
||||
if (IsHandleValid(conOut))
|
||||
{
|
||||
_ = SetStdHandle(stdHandle, conOut);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool IsHandleValid(nint handle)
|
||||
{
|
||||
return handle != 0 && handle != -1;
|
||||
}
|
||||
|
||||
private static string[] NormalizeInternalArguments(string[] args, out bool isMitigatedChild)
|
||||
{
|
||||
isMitigatedChild = false;
|
||||
var trustedMitigatedChild = string.Equals(
|
||||
Environment.GetEnvironmentVariable(MitigatedChildEnvironment),
|
||||
"1",
|
||||
StringComparison.Ordinal);
|
||||
if (args.Length == 0)
|
||||
{
|
||||
return args;
|
||||
@@ -114,7 +266,7 @@ internal static partial class Program
|
||||
{
|
||||
if (string.Equals(arg, MitigatedChildFlag, StringComparison.Ordinal))
|
||||
{
|
||||
isMitigatedChild = true;
|
||||
isMitigatedChild = trustedMitigatedChild;
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -153,9 +305,11 @@ internal static partial class Program
|
||||
var commandLine = BuildCommandLine(processPath, childArgs);
|
||||
var startupInfoEx = new STARTUPINFOEX();
|
||||
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
|
||||
ConfigureInheritedStdHandles(ref startupInfoEx.StartupInfo);
|
||||
|
||||
nint attributeList = 0;
|
||||
nint mitigationPolicies = 0;
|
||||
var previousChildEnvironment = Environment.GetEnvironmentVariable(MitigatedChildEnvironment);
|
||||
try
|
||||
{
|
||||
nuint attributeListSize = 0;
|
||||
@@ -163,7 +317,9 @@ internal static partial class Program
|
||||
attributeList = Marshal.AllocHGlobal((nint)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;
|
||||
@@ -171,8 +327,7 @@ internal static partial class Program
|
||||
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
var policy2 =
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF |
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
|
||||
|
||||
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
||||
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
||||
@@ -187,24 +342,31 @@ internal static partial class Program
|
||||
0,
|
||||
0))
|
||||
{
|
||||
return false;
|
||||
childExitCode = 5;
|
||||
Console.Error.WriteLine($"[ERROR] Failed to apply mitigation attributes: {Marshal.GetLastWin32Error()}");
|
||||
return true;
|
||||
}
|
||||
|
||||
var cmdLineBuilder = new StringBuilder(commandLine);
|
||||
nint jobHandle = 0;
|
||||
if (!CreateProcessW(
|
||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, "1");
|
||||
var created = CreateProcessW(
|
||||
processPath,
|
||||
cmdLineBuilder,
|
||||
0,
|
||||
0,
|
||||
false,
|
||||
true,
|
||||
EXTENDED_STARTUPINFO_PRESENT,
|
||||
0,
|
||||
Environment.CurrentDirectory,
|
||||
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
|
||||
@@ -258,6 +420,8 @@ internal static partial class Program
|
||||
}
|
||||
finally
|
||||
{
|
||||
Environment.SetEnvironmentVariable(MitigatedChildEnvironment, previousChildEnvironment);
|
||||
|
||||
if (attributeList != 0)
|
||||
{
|
||||
DeleteProcThreadAttributeList(attributeList);
|
||||
@@ -271,6 +435,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)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
@@ -373,27 +769,30 @@ internal static partial class Program
|
||||
|
||||
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(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug ""E:\Games\...\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 --log-file ""E:\Games\...\eboot.bin""");
|
||||
}
|
||||
|
||||
private static bool TryParseArguments(
|
||||
string[] args,
|
||||
out string ebootPath,
|
||||
out SharpEmuRuntimeOptions runtimeOptions,
|
||||
out LogLevel logLevel)
|
||||
out LogLevel logLevel,
|
||||
out string? logFilePath)
|
||||
{
|
||||
if (args.Length == 0)
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
var strictDynlibResolution = false;
|
||||
var importTraceLimit = 0;
|
||||
var cpuEngine = CpuExecutionEngine.NativeOnly;
|
||||
logFilePath = null;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
var pathTokens = new List<string>(args.Length);
|
||||
for (var i = 0; i < args.Length; i++)
|
||||
@@ -423,6 +822,7 @@ internal static partial class Program
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -436,6 +836,7 @@ internal static partial class Program
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -443,6 +844,23 @@ internal static partial class Program
|
||||
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=";
|
||||
if (argument.StartsWith(logLevelPrefix, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
@@ -466,6 +884,7 @@ internal static partial class Program
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -488,11 +907,27 @@ internal static partial class Program
|
||||
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))
|
||||
{
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -504,6 +939,7 @@ internal static partial class Program
|
||||
ebootPath = string.Empty;
|
||||
runtimeOptions = default;
|
||||
logLevel = SharpEmuLog.MinimumLevel;
|
||||
logFilePath = null;
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -605,6 +1041,56 @@ internal static partial class Program
|
||||
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)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool InitializeProcThreadAttributeList(
|
||||
@@ -670,4 +1156,36 @@ internal static partial class Program
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(nint handle);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern nint GetConsoleWindow();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AttachConsole(int processId);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AllocConsole();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern nint GetStdHandle(int stdHandle);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetStdHandle(int stdHandle, nint handle);
|
||||
|
||||
[DllImport("kernel32.dll", 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)]
|
||||
private static extern nint CreateFileW(
|
||||
string fileName,
|
||||
uint desiredAccess,
|
||||
uint shareMode,
|
||||
nint securityAttributes,
|
||||
uint creationDisposition,
|
||||
uint flagsAndAttributes,
|
||||
nint templateFile);
|
||||
}
|
||||
|
||||
@@ -7,11 +7,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" />
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<!-- GUI subsystem so starting without arguments opens the frontend with no
|
||||
console window; CLI mode re-attaches to the parent terminal's console. -->
|
||||
<OutputType>WinExe</OutputType>
|
||||
<AssemblyName>SharpEmu</AssemblyName>
|
||||
<RuntimeIdentifiers>win-x64;linux-x64;osx-arm64</RuntimeIdentifiers>
|
||||
<SelfContained>true</SelfContained>
|
||||
@@ -21,6 +24,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
<Version>0.0.1</Version>
|
||||
<ServerGarbageCollection>true</ServerGarbageCollection>
|
||||
<ConcurrentGarbageCollection>true</ConcurrentGarbageCollection>
|
||||
<TieredPGO>true</TieredPGO>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(Configuration)' == 'Release'">
|
||||
@@ -30,12 +36,39 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
|
||||
<ApplicationIcon>..\..\assets\images\SharpEmu.ico</ApplicationIcon>
|
||||
<Win32Icon>..\..\assets\images\SharpEmu.ico</Win32Icon>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Content Include="..\..\LICENSE.txt">
|
||||
<CopyToOutputDirectory>Always</CopyToOutputDirectory>
|
||||
<TargetPath>LICENSE.txt</TargetPath>
|
||||
<Visible>False</Visible>
|
||||
</Content>
|
||||
<Content Include="..\SharpEmu.GUI\Languages\*.json" Condition="'$(Configuration)' != 'Release'">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
<TargetPath>Languages\%(Filename)%(Extension)</TargetPath>
|
||||
<Visible>False</Visible>
|
||||
</Content>
|
||||
</ItemGroup>
|
||||
|
||||
<!-- Keep glfw as a loose file next to the executable; every other native
|
||||
library is embedded into the single-file bundle. -->
|
||||
<Target Name="KeepGlfwOutsideSingleFile" AfterTargets="ComputeResolvedFilesToPublishList">
|
||||
<ItemGroup>
|
||||
<_GlfwPublishFiles Include="@(ResolvedFileToPublish)"
|
||||
Condition="$([System.String]::Copy('%(ResolvedFileToPublish.Filename)').StartsWith('glfw'))" />
|
||||
<ResolvedFileToPublish Remove="@(_GlfwPublishFiles)" />
|
||||
<ResolvedFileToPublish Include="@(_GlfwPublishFiles)">
|
||||
<ExcludeFromSingleFile>true</ExcludeFromSingleFile>
|
||||
</ResolvedFileToPublish>
|
||||
</ItemGroup>
|
||||
</Target>
|
||||
|
||||
</Project>
|
||||
|
||||
@@ -8,6 +8,105 @@
|
||||
"resolved": "10.0.3",
|
||||
"contentHash": "0B6nZyCHWXnvmlB559oduOspVdNOnpNXPjhpWVMovLPAsDVG7A4jJR9rzECf67JUzxP8/ee/wA8clwIzJcWNFA=="
|
||||
},
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.BuildServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.2",
|
||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||
},
|
||||
"Avalonia.FreeDesktop": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Tmds.DBus.Protocol": "0.21.3"
|
||||
}
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Remote.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
||||
},
|
||||
"Avalonia.Skia": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"HarfBuzzSharp": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
||||
"SkiaSharp": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
||||
}
|
||||
},
|
||||
"Avalonia.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
||||
}
|
||||
},
|
||||
"Avalonia.X11": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.FreeDesktop": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
||||
"dependencies": {
|
||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"MicroCom.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.11.0",
|
||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||
},
|
||||
"Microsoft.DotNet.PlatformAbstractions": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.1.6",
|
||||
@@ -59,6 +158,38 @@
|
||||
"Silk.NET.Windowing.Common": "2.23.0"
|
||||
}
|
||||
},
|
||||
"SkiaSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||
"dependencies": {
|
||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
@@ -73,14 +204,29 @@
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.gui": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"Avalonia": "[11.3.18, )",
|
||||
"Avalonia.Desktop": "[11.3.18, )",
|
||||
"Avalonia.Fonts.Inter": "[11.3.18, )",
|
||||
"Avalonia.Themes.Fluent": "[11.3.18, )",
|
||||
"SharpEmu.Logging": "[1.0.0, )",
|
||||
"Tmds.DBus.Protocol": "[0.21.3, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
@@ -88,6 +234,48 @@
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
},
|
||||
"Avalonia": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "11.3.18",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Native": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18",
|
||||
"Avalonia.Win32": "11.3.18",
|
||||
"Avalonia.X11": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Iced": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[1.21.0, )",
|
||||
@@ -103,6 +291,16 @@
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
@@ -122,9 +320,61 @@
|
||||
"Silk.NET.Windowing.Common": "2.23.0",
|
||||
"Silk.NET.Windowing.Glfw": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[0.21.3, )",
|
||||
"resolved": "0.21.3",
|
||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||
}
|
||||
},
|
||||
"net10.0/linux-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
@@ -132,6 +382,52 @@
|
||||
}
|
||||
},
|
||||
"net10.0/osx-arm64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
@@ -139,6 +435,52 @@
|
||||
}
|
||||
},
|
||||
"net10.0/win-x64": {
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"Ultz.Native.GLFW": {
|
||||
"type": "Transitive",
|
||||
"resolved": "3.4.0",
|
||||
|
||||
@@ -7,11 +7,14 @@ using SharpEmu.Core.Cpu.Native;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu;
|
||||
|
||||
public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Dispatcher");
|
||||
|
||||
private enum EntryFrameKind
|
||||
{
|
||||
ProcessEntry,
|
||||
@@ -80,8 +83,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string processImageName = "eboot.bin",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchEntry START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
Log.Debug("=== DispatchEntry START ===");
|
||||
Log.Debug($"entryPoint=0x{entryPoint:X16}, generation={generation}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -89,8 +92,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchEntry: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -103,8 +105,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
string moduleName = "module",
|
||||
CpuExecutionOptions executionOptions = default)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] === DispatchModuleInitializer START ===");
|
||||
Console.Error.WriteLine($"[DISPATCHER] moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
Log.Debug("=== DispatchModuleInitializer START ===");
|
||||
Log.Debug($"moduleInit=0x{entryPoint:X16}, generation={generation}, module={moduleName}");
|
||||
|
||||
try
|
||||
{
|
||||
@@ -119,8 +121,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[DISPATCHER] FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Stack trace: {ex.StackTrace}");
|
||||
Log.Critical($"FATAL EXCEPTION in DispatchModuleInitializer: {ex.GetType().Name}: {ex.Message}", ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
@@ -134,7 +135,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
CpuExecutionOptions executionOptions = default,
|
||||
EntryFrameKind frameKind = EntryFrameKind.ProcessEntry)
|
||||
{
|
||||
Console.Error.WriteLine("[DISPATCHER] DispatchEntryCore STARTING...");
|
||||
Log.Debug("DispatchEntryCore STARTING...");
|
||||
|
||||
LastEntryPoint = entryPoint;
|
||||
LastTrapInfo = null;
|
||||
@@ -306,7 +307,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
LastMilestoneLog,
|
||||
Environment.NewLine,
|
||||
$"CpuEngine native-only failed: {backendError}");
|
||||
Console.Error.WriteLine($"[DISPATCHER] Native backend FAILED: {backendError}");
|
||||
Log.Error($"Native backend FAILED: {backendError}");
|
||||
return FailEarly(
|
||||
OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED,
|
||||
CpuExitReason.NativeBackendUnavailable);
|
||||
@@ -415,8 +416,8 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
|
||||
const ulong entryParamsSize = 0x20;
|
||||
var entryParamsAddress = AlignDown(argv0Address - entryParamsSize, 16);
|
||||
if (!TryWriteUInt32(context, entryParamsAddress + 0x00, 1) ||
|
||||
!TryWriteUInt32(context, entryParamsAddress + 0x04, 0) ||
|
||||
if (!context.TryWriteUInt32(entryParamsAddress + 0x00, 1) ||
|
||||
!context.TryWriteUInt32(entryParamsAddress + 0x04, 0) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x08, argv0Address) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x10, 0) ||
|
||||
!context.TryWriteUInt64(entryParamsAddress + 0x18, 0))
|
||||
@@ -456,13 +457,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
|
||||
return value & ~(alignment - 1);
|
||||
}
|
||||
|
||||
private static bool TryWriteUInt32(CpuContext context, ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return context.Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
private static string BuildEntryFrameDiagnostic(
|
||||
ulong entryPoint,
|
||||
CpuContext context,
|
||||
|
||||
@@ -8,6 +8,7 @@ using System.Linq;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Cpu.Native;
|
||||
|
||||
@@ -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()
|
||||
{
|
||||
if (_recentImportTraceCount == 0)
|
||||
{
|
||||
return;
|
||||
}
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Recent import calls ({_recentImportTraceCount}):");
|
||||
Log.Info($" Recent import calls ({_recentImportTraceCount}):");
|
||||
int num = (_recentImportTraceWriteIndex - _recentImportTraceCount + _recentImportTrace.Length) % _recentImportTrace.Length;
|
||||
for (int i = 0; i < _recentImportTraceCount; i++)
|
||||
{
|
||||
@@ -51,8 +117,8 @@ public sealed partial class DirectExecutionBackend
|
||||
var entry = _recentImportTrace[num2];
|
||||
if (!string.IsNullOrEmpty(entry.Nid))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][INFO] #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||
Log.Info(
|
||||
$" #{entry.DispatchIndex} nid={entry.Nid} ret=0x{entry.ReturnRip:X16} " +
|
||||
$"rdi=0x{entry.Arg0:X16} rsi=0x{entry.Arg1:X16} rdx=0x{entry.Arg2:X16}");
|
||||
}
|
||||
}
|
||||
@@ -91,12 +157,12 @@ public sealed partial class DirectExecutionBackend
|
||||
list.Add(num7);
|
||||
if (num2 < 32)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][INFO] Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
|
||||
Log.Info($"Suspicious unresolved pointer: slot=0x{num7:X16} value=0x{value2:X16}");
|
||||
num2++;
|
||||
}
|
||||
if (num >= 16384)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
|
||||
Log.Warning($"Suspicious unresolved pointer scan reached cap ({16384}); truncating.");
|
||||
return list;
|
||||
}
|
||||
}
|
||||
@@ -106,7 +172,7 @@ public sealed partial class DirectExecutionBackend
|
||||
}
|
||||
if (num != 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][WARNING] Suspicious unresolved pointer hits: {num}");
|
||||
Log.Warning($"Suspicious unresolved pointer hits: {num}");
|
||||
}
|
||||
return list;
|
||||
}
|
||||
@@ -121,18 +187,18 @@ public sealed partial class DirectExecutionBackend
|
||||
Span<byte> destination = stackalloc byte[128];
|
||||
if (!cpuContext.Memory.TryRead(returnRip, destination))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
|
||||
Log.Debug($"Import#{dispatchIndex} return-rip probe: unreadable @0x{returnRip:X16}");
|
||||
return;
|
||||
}
|
||||
string value = BitConverter.ToString(destination.ToArray()).Replace("-", " ");
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
|
||||
Log.Debug($"Import#{dispatchIndex} return-rip bytes @0x{returnRip:X16}: {value}");
|
||||
if (destination[0] == byte.MaxValue && (destination[1] == 21 || destination[1] == 37))
|
||||
{
|
||||
int num = BitConverter.ToInt32(destination.Slice(2, 4));
|
||||
ulong num2 = returnRip + 6 + (ulong)num;
|
||||
if (cpuContext.TryReadUInt64(num2, out var value2))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
|
||||
Log.Debug($"Import#{dispatchIndex} return-rip slot: [0x{num2:X16}] = 0x{value2:X16}");
|
||||
}
|
||||
}
|
||||
if (destination[0] == 72 && destination[1] == 139 && destination[2] == 5)
|
||||
@@ -141,7 +207,7 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong num4 = returnRip + 7 + (ulong)num3;
|
||||
if (cpuContext.TryReadUInt64(num4, out var value3))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
|
||||
Log.Debug($"Import#{dispatchIndex} return-rip mov-slot: [0x{num4:X16}] = 0x{value3:X16}");
|
||||
}
|
||||
}
|
||||
for (int i = 0; i + 6 <= destination.Length; i++)
|
||||
@@ -153,7 +219,7 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong num7 = num6 + 6 + (ulong)num5;
|
||||
if (cpuContext.TryReadUInt64(num7, out var value4))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
|
||||
Log.Debug($"Import#{dispatchIndex} near-indirect @{num6:X16}: slot=0x{num7:X16} val=0x{value4:X16}");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -168,32 +234,33 @@ public sealed partial class DirectExecutionBackend
|
||||
int rel32 = BitConverter.ToInt32(destination.Slice(i + 1, 4));
|
||||
ulong callRip = returnRip + (ulong)i;
|
||||
ulong target = unchecked((ulong)((long)(callRip + 5) + rel32));
|
||||
Console.Error.WriteLine($"[LOADER][TRACE] Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||
Log.Debug($"Import#{dispatchIndex} near-call @{callRip:X16}: target=0x{target:X16}");
|
||||
var importEntries = _importEntries;
|
||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
||||
{
|
||||
if (_importEntries[importIndex].Address != target)
|
||||
if (importEntries[importIndex].Address != target)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
string nid = importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call import: index={importIndex} nid={nid}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
if (cpuContext.Memory.TryRead(target, targetBytes))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call target bytes @0x{target:X16}: " +
|
||||
BitConverter.ToString(targetBytes.ToArray()).Replace("-", " "));
|
||||
if (targetBytes[0] == 0xFF && targetBytes[1] == 0x25)
|
||||
{
|
||||
@@ -201,25 +268,25 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong slot = unchecked((ulong)((long)(target + 6) + slotRel32));
|
||||
if (cpuContext.TryReadUInt64(slot, out var slotTarget))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||
for (int importIndex = 0; importIndex < _importEntries.Length; importIndex++)
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT slot: [0x{slot:X16}] = 0x{slotTarget:X16}");
|
||||
for (int importIndex = 0; importIndex < importEntries.Length; importIndex++)
|
||||
{
|
||||
if (_importEntries[importIndex].Address != slotTarget)
|
||||
if (importEntries[importIndex].Address != slotTarget)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
string nid = _importEntries[importIndex].Nid;
|
||||
string nid = importEntries[importIndex].Nid;
|
||||
if (_moduleManager.TryGetExport(nid, out var export))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} {export.LibraryName}:{export.Name} ({nid})");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TRACE] Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
||||
Log.Debug(
|
||||
$"Import#{dispatchIndex} near-call PLT import: index={importIndex} nid={nid}");
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
@@ -103,6 +103,15 @@ public sealed partial class DirectExecutionBackend
|
||||
ulong rip = ReadCtxU64(contextRecord, 248);
|
||||
ulong rsp = ReadCtxU64(contextRecord, 152);
|
||||
|
||||
// Thread-mode probe: a hardware exception raised while this thread is inside
|
||||
// the managed import gateway means the VEH->managed reentry happened from
|
||||
// cooperative GC mode — a ReversePInvokeBadTransition candidate.
|
||||
if (LogThreadMode && _threadModeGatewayDepth > 0)
|
||||
{
|
||||
TraceThreadMode(
|
||||
$"veh_in_gateway code=0x{exceptionCode:X8} rip=0x{rip:X16} gateway_depth={_threadModeGatewayDepth}");
|
||||
}
|
||||
|
||||
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
|
||||
{
|
||||
return -1;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -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
@@ -45,7 +45,13 @@ public static class Ps5ParamJsonReader
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = JsonDocument.Parse(data);
|
||||
ReadOnlyMemory<byte> json = data;
|
||||
if (json.Span.StartsWith("\uFEFF"u8))
|
||||
{
|
||||
json = json[3..];
|
||||
}
|
||||
|
||||
using var doc = JsonDocument.Parse(json);
|
||||
return TryReadPs5Param(doc.RootElement);
|
||||
}
|
||||
catch (JsonException)
|
||||
@@ -56,12 +62,15 @@ public static class Ps5ParamJsonReader
|
||||
|
||||
private static (string? Title, string? TitleId, string? Version) TryReadPs5Param(JsonElement root)
|
||||
{
|
||||
string? titleId = root.TryGetProperty("titleId", out var eTid) ? eTid.GetString() : null;
|
||||
if (root.ValueKind != JsonValueKind.Object)
|
||||
return (null, null, null);
|
||||
|
||||
var titleId = GetString(root, "titleId");
|
||||
|
||||
string? ver =
|
||||
(root.TryGetProperty("contentVersion", out var cv) ? cv.GetString() : null)
|
||||
?? (root.TryGetProperty("masterVersion", out var mv) ? mv.GetString() : null)
|
||||
?? (root.TryGetProperty("targetContentVersion", out var tv) ? tv.GetString() : null);
|
||||
GetString(root, "contentVersion")
|
||||
?? GetString(root, "masterVersion")
|
||||
?? GetString(root, "targetContentVersion");
|
||||
|
||||
string? title = ExtractTitleName(root);
|
||||
|
||||
@@ -70,34 +79,49 @@ public static class Ps5ParamJsonReader
|
||||
|
||||
private static string? ExtractTitleName(JsonElement root)
|
||||
{
|
||||
if (!root.TryGetProperty("localizedParameters", out var lp))
|
||||
if ((!root.TryGetProperty("localizedParameters", out var lp) || lp.ValueKind != JsonValueKind.Object) &&
|
||||
root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (root.TryGetProperty("disc", out var disc) && disc.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
disc.TryGetProperty("localizedParameters", out lp);
|
||||
}
|
||||
disc.TryGetProperty("localizedParameters", out lp);
|
||||
}
|
||||
|
||||
if (lp.ValueKind != JsonValueKind.Object)
|
||||
return null;
|
||||
|
||||
string? defLang = lp.TryGetProperty("defaultLanguage", out var dl) ? dl.GetString() : null;
|
||||
var defLang = GetString(lp, "defaultLanguage");
|
||||
|
||||
if (!string.IsNullOrEmpty(defLang))
|
||||
{
|
||||
if (lp.TryGetProperty(defLang, out var langObj) && langObj.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (langObj.TryGetProperty("titleName", out var tn))
|
||||
return tn.GetString();
|
||||
var title = GetString(langObj, "titleName");
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
if (lp.TryGetProperty("en-US", out var en) && en.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
if (en.TryGetProperty("titleName", out var tn2))
|
||||
return tn2.GetString();
|
||||
var title = GetString(en, "titleName");
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
return title;
|
||||
}
|
||||
|
||||
foreach (var property in lp.EnumerateObject())
|
||||
{
|
||||
if (property.Value.ValueKind == JsonValueKind.Object)
|
||||
{
|
||||
var title = GetString(property.Value, "titleName");
|
||||
if (!string.IsNullOrWhiteSpace(title))
|
||||
return title;
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static string? GetString(JsonElement parent, string propertyName) =>
|
||||
parent.TryGetProperty(propertyName, out var value) && value.ValueKind == JsonValueKind.String
|
||||
? value.GetString()
|
||||
: null;
|
||||
}
|
||||
|
||||
@@ -10,11 +10,13 @@ using SharpEmu.Core;
|
||||
using SharpEmu.Core.Cpu;
|
||||
using SharpEmu.Core.Memory;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Loader;
|
||||
|
||||
public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Loader");
|
||||
private const uint SelfMagic = 0x4F153D1D;
|
||||
private const ulong SelfSegmentFlag = 0x800;
|
||||
private const int PageSize = 0x1000;
|
||||
@@ -155,7 +157,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
}
|
||||
|
||||
var tlsModuleId = _nextTlsModuleId == 0 ? 1u : _nextTlsModuleId;
|
||||
Console.Error.WriteLine($"[LOADER][TLS] load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
|
||||
Log.Debug($"TLS load_start clear={clearVirtualMemory} next={_nextTlsModuleId} assigned={tlsModuleId}");
|
||||
|
||||
var loadContext = ParseLayout(imageData);
|
||||
var elfHeader = ReadUnmanaged<ElfHeader>(imageData, loadContext.ElfOffset);
|
||||
@@ -255,7 +257,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
_nextTlsModuleId++;
|
||||
}
|
||||
Console.Error.WriteLine($"[LOADER][TLS] load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||
Log.Debug($"TLS load_done assigned={tlsModuleId} next={_nextTlsModuleId}");
|
||||
|
||||
return new SelfImage(
|
||||
loadContext.IsSelf,
|
||||
@@ -403,15 +405,15 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
var virtualAddress = header.VirtualAddress + imageBase;
|
||||
|
||||
Console.Error.WriteLine($"[LOADER] Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
||||
Log.Debug($"Segment {index}: VAddr=0x{virtualAddress:X16}, FileSize=0x{header.FileSize:X}, MemSize=0x{header.MemorySize:X}, Align=0x{header.Alignment:X}");
|
||||
if (header.Alignment > 1)
|
||||
{
|
||||
var vaddrMod = virtualAddress % header.Alignment;
|
||||
var offsetMod = header.Offset % header.Alignment;
|
||||
if (vaddrMod != offsetMod)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] WARNING: Segment {index} ELF alignment mismatch! " +
|
||||
Log.Warning(
|
||||
$"Segment {index} ELF alignment mismatch! " +
|
||||
$"VAddr=0x{virtualAddress:X}, Offset=0x{header.Offset:X}, Align=0x{header.Alignment:X}, " +
|
||||
$"VAddr%Align=0x{vaddrMod:X}, Offset%Align=0x{offsetMod:X}");
|
||||
}
|
||||
@@ -671,13 +673,13 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (descriptor.ValueKind == RelocationValueKind.TlsModuleId)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][TLS] Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||
Log.Debug(
|
||||
$"Patching DTPMOD64 at 0x{descriptor.TargetAddress:X} with module id 0x{targetValue:X}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER] !!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||
Console.Error.WriteLine($"[LOADER] SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||
Log.Warning($"!!! CRITICAL !!! Patching address 0x{descriptor.TargetAddress:X} with INVALID value 0x{targetValue:X} for NID {descriptor.ImportNid ?? "(null)"}");
|
||||
Log.Warning($" SymbolValue=0x{descriptor.SymbolValue:X}, Addend=0x{descriptor.Addend:X}, StubAddress=0x{(addressesByNid.TryGetValue(descriptor.ImportNid ?? "", out var sa) ? sa : 0):X}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -689,8 +691,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
if (descriptor.TargetAddress >= 0x00000008030FC300UL &&
|
||||
descriptor.TargetAddress <= 0x00000008030FC3F0UL)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
||||
Log.Debug(
|
||||
$"[RELOC] target=0x{descriptor.TargetAddress:X16} value=0x{targetValue:X16} addend=0x{descriptor.Addend:X} nid={(descriptor.ImportNid ?? "<sym>")}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -783,15 +785,15 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER][FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||
Log.Debug(
|
||||
$"[FOCUS][SCAN] off=0x{relocation.Offset:X16} type={relocation.Type} sym={relocation.SymbolIndex} addend=0x{relocation.Addend:X}");
|
||||
}
|
||||
|
||||
if (!IsSupportedRelocationType(relocation.Type))
|
||||
{
|
||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] unsupported type={relocation.Type}");
|
||||
Log.Debug($"[FOCUS][SKIP] unsupported type={relocation.Type}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -800,7 +802,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (IsFocusRelocationOffset(relocation.Offset, imageBase))
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][FOCUS][SKIP] target address not mapped");
|
||||
Log.Debug("[FOCUS][SKIP] target address not mapped");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -840,7 +842,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
||||
Log.Debug($"[FOCUS][SKIP] symbol read failed index={symbolIndex}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -852,10 +854,9 @@ public sealed class SelfLoader : ISelfLoader
|
||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||
if (symbolAddress == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
Log.Warning(
|
||||
$"Skipping local relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||
continue;
|
||||
}
|
||||
|
||||
descriptors.Add(new RelocationDescriptor(
|
||||
@@ -873,10 +874,9 @@ public sealed class SelfLoader : ISelfLoader
|
||||
var symbolAddress = ResolveMappedAddressOrFallback(virtualMemory, symbol.Value, imageBase);
|
||||
if (symbolAddress == 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
Log.Warning(
|
||||
$"Skipping relocation with invalid symbol value 0x{symbol.Value:X} " +
|
||||
$"at target 0x{targetAddress:X16}, type={relocation.Type}, sym={symbolIndex}");
|
||||
continue;
|
||||
}
|
||||
|
||||
descriptors.Add(new RelocationDescriptor(
|
||||
@@ -893,7 +893,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] bind={symbolBind} not importable");
|
||||
Log.Debug($"[FOCUS][SKIP] bind={symbolBind} not importable");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -902,7 +902,7 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
if (targetAddress >= FocusRelocGuestStart && targetAddress <= FocusRelocGuestEnd)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
||||
Log.Debug($"[FOCUS][SKIP] symbol name read failed offset={symbol.NameOffset}");
|
||||
}
|
||||
continue;
|
||||
}
|
||||
@@ -950,8 +950,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
if (sectionSymbols > 0 || dynamicSymbols > 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
||||
Log.Info(
|
||||
$"Runtime symbol index populated: section={sectionSymbols}, dynamic={dynamicSymbols}, total={runtimeSymbols.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1025,8 +1025,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
if (preInitializers.Count != 0 || initializers.Count != 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
||||
Log.Info(
|
||||
$"Initializers discovered: preinit={preInitializers.Count}, init={initializers.Count}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1409,8 +1409,8 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
var requiredBytes = checked((ulong)orderedImportNids.Count * ImportStubSlotSize);
|
||||
var mapSize = AlignUp(Math.Max(requiredBytes, (ulong)PageSize), PageSize);
|
||||
Console.Error.WriteLine(
|
||||
$"[LOADER] CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||
Log.Debug(
|
||||
$"CreateImportStubMapping: nids={orderedImportNids.Count}, required=0x{requiredBytes:X}, map_size=0x{mapSize:X}");
|
||||
if (mapSize > int.MaxValue)
|
||||
{
|
||||
throw new NotSupportedException("Import stub mapping exceeds 2 GB and is not supported.");
|
||||
@@ -1842,16 +1842,16 @@ public sealed class SelfLoader : ISelfLoader
|
||||
tableBytes = GC.AllocateUninitializedArray<byte>((int)size);
|
||||
|
||||
var guestAddr = location + imageBase;
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
||||
Log.Debug($"TryLoadTableBytes: trying guest address 0x{guestAddr:X}");
|
||||
if (virtualMemory.TryRead(guestAddr, tableBytes))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
||||
Log.Debug($"TryLoadTableBytes: loaded from guest memory at 0x{guestAddr:X}");
|
||||
return true;
|
||||
}
|
||||
|
||||
if (virtualMemory.TryRead(location, tableBytes))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
||||
Log.Debug($"TryLoadTableBytes: loaded from absolute guest address 0x{location:X}");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -1859,11 +1859,11 @@ public sealed class SelfLoader : ISelfLoader
|
||||
{
|
||||
var slice = elfData.Slice((int)location, (int)size);
|
||||
tableBytes = slice.ToArray();
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
||||
Log.Debug($"TryLoadTableBytes: loaded from elfData as file offset at 0x{location:X}");
|
||||
return true;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER] TryLoadTableBytes: FAILED for location 0x{location:X}");
|
||||
Log.Warning($"TryLoadTableBytes: FAILED for location 0x{location:X}");
|
||||
tableBytes = Array.Empty<byte>();
|
||||
return false;
|
||||
}
|
||||
@@ -1920,17 +1920,17 @@ public sealed class SelfLoader : ISelfLoader
|
||||
|
||||
private static ulong ResolveMappedAddressOrFallback(IVirtualMemory virtualMemory, ulong address, ulong imageBase)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TEST] ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
||||
Log.Debug($"ResolveMappedAddressOrFallback addr=0x{address:X} imageBase=0x{imageBase:X16}");
|
||||
|
||||
if (address == 0)
|
||||
{
|
||||
Console.Error.WriteLine("[LOADER][TEST] -> return 0 (null)");
|
||||
Log.Debug("-> return 0 (null)");
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (TryResolveMappedAddress(virtualMemory, address, imageBase, 1, out var resolved))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> resolved raw 0x{resolved:X16}");
|
||||
Log.Debug($"-> resolved raw 0x{resolved:X16}");
|
||||
return resolved;
|
||||
}
|
||||
|
||||
@@ -1939,18 +1939,18 @@ public sealed class SelfLoader : ISelfLoader
|
||||
var rebased = address + imageBase;
|
||||
if (TryResolveMappedAddress(virtualMemory, rebased, imageBase, 1, out var resolvedRebased))
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> resolved rebased 0x{resolvedRebased:X16}");
|
||||
Log.Debug($"-> resolved rebased 0x{resolvedRebased:X16}");
|
||||
return resolvedRebased;
|
||||
}
|
||||
}
|
||||
|
||||
if (address < 0x10000)
|
||||
{
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> reject small 0x{address:X}");
|
||||
Log.Debug($"-> reject small 0x{address:X}");
|
||||
return 0;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[LOADER][TEST] -> fallback raw 0x{address:X}");
|
||||
Log.Debug($"-> fallback raw 0x{address:X}");
|
||||
return address;
|
||||
}
|
||||
|
||||
|
||||
@@ -4,14 +4,19 @@
|
||||
using System.Runtime.InteropServices;
|
||||
using SharpEmu.Core.Loader;
|
||||
using SharpEmu.HLE;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.Core.Memory;
|
||||
|
||||
public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryAllocator, IDisposable
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("VMEM");
|
||||
|
||||
private readonly ReaderWriterLockSlim _gate = new(LockRecursionPolicy.SupportsRecursion);
|
||||
private readonly object _guestAllocationGate = new();
|
||||
private readonly object _allocationSearchHintGate = new();
|
||||
private readonly List<MemoryRegion> _regions = new();
|
||||
private readonly Dictionary<(ulong DesiredAddress, ulong Alignment, bool Executable), ulong> _allocationSearchHints = new();
|
||||
private readonly Dictionary<ulong, ProgramHeaderFlags> _pageProtections = new();
|
||||
private bool _disposed;
|
||||
private const ulong PageSize = 0x1000;
|
||||
@@ -19,7 +24,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
private const ulong GuestAllocationArenaSize = 0x0100_0000;
|
||||
private const ulong GuestAllocationArenaStartOffset = PageSize;
|
||||
private const ulong LargeDataReserveThreshold = 0x4000_0000UL; // 1 GiB
|
||||
private const ulong LazyReservePrimeBytes = 0x5000_0000UL; // 1.25 GiB
|
||||
private const ulong FullCommitRegionLimit = 4UL << 30;
|
||||
private const ulong DefaultLazyReservePrimeBytes = 0x0400_0000UL; // 64 MiB
|
||||
private const ulong LazyReservePrimeChunkBytes = 0x0200_0000UL; // 32 MiB
|
||||
|
||||
private const uint MEM_COMMIT = 0x1000;
|
||||
@@ -35,6 +41,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private ulong _guestAllocationArenaBase;
|
||||
private ulong _guestAllocationOffset;
|
||||
private static readonly ulong LazyReservePrimeBytes = ResolveLazyReservePrimeBytes();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern void* VirtualAlloc(void* lpAddress, nuint dwSize, uint flAllocationType, uint flProtect);
|
||||
@@ -51,7 +58,11 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
private static extern nuint VirtualQuery(void* lpAddress, out MemoryBasicInformation64 lpBuffer, nuint dwLength);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern void FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||
private static extern void* GetCurrentProcess();
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool FlushInstructionCache(void* hProcess, void* lpBaseAddress, nuint dwSize);
|
||||
|
||||
public bool TryAllocateAtExact(ulong desiredAddress, ulong size, bool executable, out ulong actualAddress)
|
||||
{
|
||||
@@ -81,7 +92,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
@@ -96,7 +107,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
|
||||
var allocationKind = executable ? "executable memory" : "data memory";
|
||||
Console.Error.WriteLine($"[VMEM] Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
||||
TraceVmem($"Allocated exact {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes)");
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -110,7 +121,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var protection = executable ? PAGE_EXECUTE_READWRITE : PAGE_READWRITE;
|
||||
var allocationType = MEM_COMMIT | MEM_RESERVE;
|
||||
var reservedOnly = false;
|
||||
var preferReserveOnly = !executable && alignedSize >= LargeDataReserveThreshold;
|
||||
var preferReserveOnly = !executable &&
|
||||
alignedSize >= LargeDataReserveThreshold &&
|
||||
alignedSize > FullCommitRegionLimit;
|
||||
|
||||
void* result = null;
|
||||
if (preferReserveOnly)
|
||||
@@ -139,7 +152,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
throw new InvalidOperationException($"Failed to allocate exact mapping at 0x{desiredAddress:X16} ({alignedSize} bytes)");
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[VMEM] Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
||||
TraceVmem($"Could not allocate at 0x{desiredAddress:X16}, trying any address...");
|
||||
result = VirtualAlloc(null, (nuint)alignedSize, allocationType, protection);
|
||||
|
||||
if (result == null)
|
||||
@@ -193,12 +206,12 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
lazyPrimeState = committedBytes == primeBytes
|
||||
? $"ok:{committedBytes:X}"
|
||||
: $"partial:{committedBytes:X}/{primeBytes:X}";
|
||||
Console.Error.WriteLine($"[VMEM] Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
||||
TraceVmem($"Primed lazy region: 0x{actualAddress:X16} - 0x{actualAddress + committedBytes:X16} ({committedBytes} bytes)");
|
||||
}
|
||||
else
|
||||
{
|
||||
lazyPrimeState = $"fail:{primeBytes:X}";
|
||||
Console.Error.WriteLine($"[VMEM] Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
||||
TraceVmem($"Failed to prime lazy region at 0x{actualAddress:X16} ({primeBytes} bytes), continuing with on-demand commit");
|
||||
}
|
||||
}
|
||||
else
|
||||
@@ -210,7 +223,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterWriteLock();
|
||||
try
|
||||
{
|
||||
_regions.Add(new MemoryRegion
|
||||
InsertRegionSorted(new MemoryRegion
|
||||
{
|
||||
VirtualAddress = actualAddress,
|
||||
Size = alignedSize,
|
||||
@@ -227,7 +240,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
var allocationKind = reservedOnly
|
||||
? "reserved data memory (lazy commit)"
|
||||
: (executable ? "executable memory" : "data memory");
|
||||
Console.Error.WriteLine($"[VMEM] Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
|
||||
TraceVmem($"Allocated {allocationKind}: 0x{actualAddress:X16} - 0x{actualAddress + alignedSize:X16} ({alignedSize} bytes) lazy_prime={lazyPrimeState}");
|
||||
|
||||
return actualAddress;
|
||||
}
|
||||
@@ -247,7 +260,8 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
var alignedSize = AlignUp(size, PageSize);
|
||||
var effectiveAlignment = Math.Max(PageSize, alignment == 0 ? PageSize : alignment);
|
||||
var cursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||
var requestedCursor = AlignUp(desiredAddress, effectiveAlignment);
|
||||
var cursor = GetAllocationSearchCursor(desiredAddress, requestedCursor, effectiveAlignment, executable);
|
||||
|
||||
for (var attempt = 0; attempt < 0x10000; attempt++)
|
||||
{
|
||||
@@ -267,6 +281,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
actualAddress = AllocateAt(cursor, alignedSize, executable, allowAlternative: false);
|
||||
if (actualAddress == cursor)
|
||||
{
|
||||
UpdateAllocationSearchCursor(desiredAddress, effectiveAlignment, executable, actualAddress + alignedSize);
|
||||
return true;
|
||||
}
|
||||
|
||||
@@ -334,6 +349,10 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
_regions.Clear();
|
||||
_pageProtections.Clear();
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
_allocationSearchHints.Clear();
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -390,7 +409,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
ApplySegmentProtection(mapStart, mapEnd, protection);
|
||||
|
||||
Console.Error.WriteLine($"[VMEM] Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
||||
TraceVmem($"Mapped segment: 0x{virtualAddress:X16} - 0x{virtualAddress + memorySize:X16} (file: {fileData.Length} bytes, prot: {protection})");
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -439,7 +458,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
if ((flags & ProgramHeaderFlags.Execute) != 0)
|
||||
{
|
||||
FlushInstructionCache(null, (void*)address, (nuint)size);
|
||||
FlushInstructionCache(GetCurrentProcess(), (void*)address, (nuint)size);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -473,30 +492,34 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)destination.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
if (TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (destination.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (region.IsReservedOnly)
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (byte* destPtr = destination)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)destination.Length, (nuint)destination.Length);
|
||||
@@ -527,36 +550,81 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected)
|
||||
{
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
var region = FindRegion(virtualAddress, (ulong)expected.Length);
|
||||
if (region is null ||
|
||||
!TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)expected.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (expected.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (region.IsReservedOnly &&
|
||||
!EnsureRangeCommitted((ulong)srcPtr, (ulong)expected.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!CanReadWithoutProtectionChange((ulong)srcPtr, (ulong)expected.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return new ReadOnlySpan<byte>(srcPtr, expected.Length).SequenceEqual(expected);
|
||||
}
|
||||
finally
|
||||
{
|
||||
_gate.ExitReadLock();
|
||||
}
|
||||
}
|
||||
|
||||
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
var requiresExclusiveAccess = false;
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
var region = FindRegion(virtualAddress, (ulong)source.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)source.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
if (TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (source.IsEmpty)
|
||||
{
|
||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (source.IsEmpty)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
if (region.IsReservedOnly)
|
||||
if (region.IsReservedOnly)
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
break;
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!CanWriteWithoutProtectionChange((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
requiresExclusiveAccess = true;
|
||||
}
|
||||
else
|
||||
{
|
||||
fixed (byte* srcPtr = source)
|
||||
{
|
||||
Buffer.MemoryCopy(srcPtr, destPtr, (nuint)source.Length, (nuint)source.Length);
|
||||
@@ -589,13 +657,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private bool TryReadExclusive(ulong virtualAddress, Span<byte> destination)
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
var region = FindRegion(virtualAddress, (ulong)destination.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)destination.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
if (!TryResolveRegionOffset(virtualAddress, (ulong)destination.Length, region, out var offset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var srcPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (!EnsureRangeCommitted((ulong)srcPtr, (ulong)destination.Length, region))
|
||||
{
|
||||
@@ -637,13 +706,14 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private bool TryWriteExclusive(ulong virtualAddress, ReadOnlySpan<byte> source)
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
var region = FindRegion(virtualAddress, (ulong)source.Length);
|
||||
if (region is not null &&
|
||||
TryResolveRegionOffset(
|
||||
virtualAddress,
|
||||
(ulong)source.Length,
|
||||
region,
|
||||
out var offset))
|
||||
{
|
||||
if (!TryResolveRegionOffset(virtualAddress, (ulong)source.Length, region, out var offset))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var destPtr = (void*)(region.VirtualAddress + offset);
|
||||
if (!EnsureRangeCommitted((ulong)destPtr, (ulong)source.Length, region))
|
||||
{
|
||||
@@ -677,7 +747,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
VirtualProtect(destPtr, (nuint)source.Length, oldProtect, out _);
|
||||
if (IsExecutableProtection(oldProtect))
|
||||
{
|
||||
FlushInstructionCache(null, destPtr, (nuint)source.Length);
|
||||
FlushInstructionCache(GetCurrentProcess(), destPtr, (nuint)source.Length);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -699,15 +769,9 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (virtualAddress >= region.VirtualAddress &&
|
||||
virtualAddress < region.VirtualAddress + region.Size)
|
||||
{
|
||||
return (void*)virtualAddress;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
return FindRegion(virtualAddress, 1) is not null
|
||||
? (void*)virtualAddress
|
||||
: null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -720,14 +784,7 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
_gate.EnterReadLock();
|
||||
try
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
if (TryResolveRegionOffset(virtualAddress, size, region, out _))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
return FindRegion(virtualAddress, size) is not null;
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -737,14 +794,48 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
|
||||
private MemoryRegion? FindRegion(ulong address, ulong size)
|
||||
{
|
||||
foreach (var region in _regions)
|
||||
var low = 0;
|
||||
var high = _regions.Count - 1;
|
||||
MemoryRegion? candidate = null;
|
||||
while (low <= high)
|
||||
{
|
||||
if (TryResolveRegionOffset(address, size, region, out _))
|
||||
var middle = low + ((high - low) >> 1);
|
||||
var region = _regions[middle];
|
||||
if (region.VirtualAddress <= address)
|
||||
{
|
||||
return region;
|
||||
candidate = region;
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle - 1;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
return candidate is not null &&
|
||||
TryResolveRegionOffset(address, size, candidate, out _)
|
||||
? candidate
|
||||
: null;
|
||||
}
|
||||
|
||||
private void InsertRegionSorted(MemoryRegion region)
|
||||
{
|
||||
var low = 0;
|
||||
var high = _regions.Count;
|
||||
while (low < high)
|
||||
{
|
||||
var middle = low + ((high - low) >> 1);
|
||||
if (_regions[middle].VirtualAddress < region.VirtualAddress)
|
||||
{
|
||||
low = middle + 1;
|
||||
}
|
||||
else
|
||||
{
|
||||
high = middle;
|
||||
}
|
||||
}
|
||||
|
||||
_regions.Insert(low, region);
|
||||
}
|
||||
|
||||
private bool TryGetOverlappingRegionEnd(ulong address, ulong size, out ulong overlapEnd)
|
||||
@@ -762,6 +853,16 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
foreach (var region in _regions)
|
||||
{
|
||||
var regionEnd = region.VirtualAddress + region.Size;
|
||||
if (region.VirtualAddress >= end)
|
||||
{
|
||||
break;
|
||||
}
|
||||
|
||||
if (regionEnd <= address)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (address < regionEnd && region.VirtualAddress < end)
|
||||
{
|
||||
overlapEnd = Math.Max(overlapEnd, regionEnd);
|
||||
@@ -776,6 +877,37 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return overlapEnd != 0;
|
||||
}
|
||||
|
||||
private ulong GetAllocationSearchCursor(
|
||||
ulong desiredAddress,
|
||||
ulong requestedCursor,
|
||||
ulong alignment,
|
||||
bool executable)
|
||||
{
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
var key = (desiredAddress, alignment, executable);
|
||||
if (_allocationSearchHints.TryGetValue(key, out var hintedCursor) &&
|
||||
hintedCursor > requestedCursor)
|
||||
{
|
||||
return AlignUp(hintedCursor, alignment);
|
||||
}
|
||||
}
|
||||
|
||||
return requestedCursor;
|
||||
}
|
||||
|
||||
private void UpdateAllocationSearchCursor(
|
||||
ulong desiredAddress,
|
||||
ulong alignment,
|
||||
bool executable,
|
||||
ulong nextCursor)
|
||||
{
|
||||
lock (_allocationSearchHintGate)
|
||||
{
|
||||
_allocationSearchHints[(desiredAddress, alignment, executable)] = AlignUp(nextCursor, alignment);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryResolveRegionOffset(ulong address, ulong size, MemoryRegion region, out ulong offset)
|
||||
{
|
||||
offset = 0;
|
||||
@@ -944,6 +1076,29 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
|
||||
return checked((value + mask) & ~mask);
|
||||
}
|
||||
|
||||
private static ulong ResolveLazyReservePrimeBytes()
|
||||
{
|
||||
var configured = Environment.GetEnvironmentVariable("SHARPEMU_LAZY_RESERVE_PRIME_MB");
|
||||
if (ulong.TryParse(configured, out var megabytes))
|
||||
{
|
||||
return megabytes == 0
|
||||
? 0
|
||||
: checked(Math.Min(megabytes, 4096UL) * 1024UL * 1024UL);
|
||||
}
|
||||
|
||||
return DefaultLazyReservePrimeBytes;
|
||||
}
|
||||
|
||||
private static void TraceVmem(string message)
|
||||
{
|
||||
if (!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_VMEM"), "1", StringComparison.Ordinal))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
Log.Debug(message);
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_disposed)
|
||||
|
||||
@@ -11,15 +11,19 @@ using SharpEmu.Libs.VideoOut;
|
||||
using SharpEmu.Libs.Kernel;
|
||||
using SharpEmu.Libs.AppContent;
|
||||
using SharpEmu.Libs.SaveData;
|
||||
using SharpEmu.Logging;
|
||||
using System.Buffers.Binary;
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using System.Text;
|
||||
using System.IO;
|
||||
|
||||
namespace SharpEmu.Core.Runtime;
|
||||
|
||||
public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Runtime");
|
||||
|
||||
private readonly record struct LoadedModuleImage(string Path, SelfImage Image);
|
||||
|
||||
private static readonly HashSet<string> PreloadSkipModules = new(StringComparer.OrdinalIgnoreCase)
|
||||
@@ -79,7 +83,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
|
||||
};
|
||||
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.Freeze();
|
||||
|
||||
@@ -128,7 +131,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
var normalizedEbootPath = Path.GetFullPath(ebootPath);
|
||||
using var app0Binding = BindApp0Root(normalizedEbootPath);
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading: {ebootPath}");
|
||||
Log.Info($"Loading: {ebootPath}");
|
||||
LastExecutionDiagnostics = null;
|
||||
LastExecutionTrace = null;
|
||||
LastSessionSummary = null;
|
||||
@@ -136,11 +139,12 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
LastMilestoneLog = null;
|
||||
KernelModuleRegistry.Reset();
|
||||
var image = LoadImage(normalizedEbootPath);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
|
||||
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version, BuildInfo.CommitSha);
|
||||
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
|
||||
LogAppBundleInfo(normalizedEbootPath, image);
|
||||
RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
|
||||
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
|
||||
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
|
||||
Log.Info($"Entry: 0x{image.EntryPoint:X16}");
|
||||
var generation = image.ElfHeader.AbiVersion == 2 ? Generation.Gen5 : Generation.Gen4;
|
||||
var activeImportStubs = new Dictionary<ulong, string>(image.ImportStubs);
|
||||
var activeRuntimeSymbols = new Dictionary<string, ulong>(image.RuntimeSymbols, StringComparer.Ordinal);
|
||||
@@ -163,7 +167,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName);
|
||||
if (initializerResult is { } failedInitializerResult)
|
||||
{
|
||||
Console.Error.WriteLine($"[RUNTIME] Initializer dispatch failed: {failedInitializerResult}");
|
||||
Log.Error($"Initializer dispatch failed: {failedInitializerResult}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -171,8 +175,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return failedInitializerResult;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatching, gen: {generation}");
|
||||
Console.Error.WriteLine($"[RUNTIME] About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
Log.Info($"Dispatching, gen: {generation}");
|
||||
Log.Debug($"About to call DispatchEntry with entryPoint=0x{image.EntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
image.EntryPoint,
|
||||
@@ -182,8 +186,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
processImageName,
|
||||
_cpuExecutionOptions);
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] DispatchEntry returned: {result}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Dispatch result: {result}");
|
||||
Log.Info($"DispatchEntry returned: {result}");
|
||||
Log.Info($"Dispatch result: {result}");
|
||||
LastExecutionTrace = _cpuDispatcher.LastImportResolutionTrace;
|
||||
LastMilestoneLog = _cpuDispatcher.LastMilestoneLog;
|
||||
LastSessionSummary = BuildSessionSummary(_cpuDispatcher.LastSessionSummary);
|
||||
@@ -354,6 +358,66 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return result;
|
||||
}
|
||||
|
||||
private static void LogAppBundleInfo(string ebootPath, SelfImage image)
|
||||
{
|
||||
var executableName = Path.GetFileName(ebootPath);
|
||||
if (string.IsNullOrWhiteSpace(executableName))
|
||||
{
|
||||
executableName = "eboot.bin";
|
||||
}
|
||||
|
||||
var displayName = string.IsNullOrWhiteSpace(image.Title) ? "(unknown)" : image.Title!.Trim();
|
||||
var titleId = string.IsNullOrWhiteSpace(image.TitleId) ? "(unknown)" : image.TitleId!.Trim();
|
||||
var version = string.IsNullOrWhiteSpace(image.Version) ? "(unknown)" : image.Version!.Trim();
|
||||
var contentId = ResolveContentId(Path.GetDirectoryName(ebootPath)) ?? "(unknown)";
|
||||
|
||||
var builder = new StringBuilder();
|
||||
builder.AppendLine("App bundle info:");
|
||||
builder.AppendLine($"- Display name: {displayName}");
|
||||
builder.AppendLine($"- Version: {version}");
|
||||
builder.AppendLine($"- Title ID: {titleId}");
|
||||
builder.AppendLine($"- Content ID: {contentId}");
|
||||
builder.AppendLine($"- Executable: {executableName}");
|
||||
builder.Append("- Platform: PlayStation 5");
|
||||
Log.Info(builder.ToString());
|
||||
}
|
||||
|
||||
private static string? ResolveContentId(string? bundleRoot)
|
||||
{
|
||||
if (string.IsNullOrEmpty(bundleRoot))
|
||||
{
|
||||
return null;
|
||||
}
|
||||
|
||||
foreach (var candidate in new[]
|
||||
{
|
||||
Path.Combine(bundleRoot, "sce_sys", "param.json"),
|
||||
Path.Combine(bundleRoot, "param.json"),
|
||||
})
|
||||
{
|
||||
if (!File.Exists(candidate))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
using var doc = System.Text.Json.JsonDocument.Parse(File.ReadAllBytes(candidate));
|
||||
if (doc.RootElement.TryGetProperty("contentId", out var contentId))
|
||||
{
|
||||
return contentId.GetString();
|
||||
}
|
||||
}
|
||||
catch (Exception ex) when (ex is IOException or System.Text.Json.JsonException)
|
||||
{
|
||||
}
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private static App0BindingScope? BindApp0Root(string normalizedEbootPath)
|
||||
{
|
||||
const string app0VariableName = "SHARPEMU_APP0_DIR";
|
||||
@@ -434,8 +498,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
moduleName = $"module#{i}";
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
Log.Info(
|
||||
$"Starting module {moduleName}: dt_init=0x{initEntryPoint:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchModuleInitializer(
|
||||
initEntryPoint,
|
||||
@@ -446,8 +510,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
_cpuExecutionOptions);
|
||||
if (result != OrbisGen2Result.ORBIS_GEN2_OK)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Module start failed: {moduleName} -> {result}");
|
||||
Log.Error(
|
||||
$"Module start failed: {moduleName} -> {result}");
|
||||
return result;
|
||||
}
|
||||
}
|
||||
@@ -468,8 +532,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return null;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
Log.Info(
|
||||
$"Running initializers for {label}: preinit={image.PreInitializerFunctions.Count}, init={image.InitializerFunctions.Count}");
|
||||
|
||||
var result = RunInitializerList(
|
||||
$"{label}:preinit",
|
||||
@@ -508,8 +572,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
continue;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
Log.Debug(
|
||||
$" Initializer {label}[{i}] -> 0x{initializerAddress:X16}");
|
||||
|
||||
var result = _cpuDispatcher.DispatchEntry(
|
||||
initializerAddress,
|
||||
@@ -543,6 +607,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
Path.Combine(ebootDirectory, "sce_module"),
|
||||
Path.Combine(ebootDirectory, "sce_modules"),
|
||||
Path.Combine(ebootDirectory, "Media", "Modules"),
|
||||
}
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.Where(Directory.Exists)
|
||||
@@ -554,7 +619,9 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
}
|
||||
|
||||
var allModulePaths = moduleDirectories
|
||||
.SelectMany(Directory.EnumerateFiles)
|
||||
.SelectMany(directory => Directory
|
||||
.EnumerateFiles(directory)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase))
|
||||
.Where(path =>
|
||||
{
|
||||
var extension = Path.GetExtension(path);
|
||||
@@ -562,7 +629,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
string.Equals(extension, ".sprx", StringComparison.OrdinalIgnoreCase);
|
||||
})
|
||||
.Distinct(StringComparer.OrdinalIgnoreCase)
|
||||
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
|
||||
.ToArray();
|
||||
|
||||
var modulePaths = allModulePaths
|
||||
@@ -575,7 +641,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
.ToArray();
|
||||
if (skippedModules.Length > 0)
|
||||
{
|
||||
Console.Error.WriteLine($"[RUNTIME] Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||
Log.Info($"Skipping {skippedModules.Length} core module(s): {string.Join(", ", skippedModules)}");
|
||||
}
|
||||
|
||||
if (modulePaths.Length == 0)
|
||||
@@ -583,8 +649,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[RUNTIME] Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||
Console.Error.WriteLine($"[RUNTIME] Loading {modulePaths.Length} module(s)...");
|
||||
Log.Debug($"Module search directories: {string.Join(", ", moduleDirectories)}");
|
||||
Log.Info($"Loading {modulePaths.Length} module(s)...");
|
||||
var loadedModules = 0;
|
||||
var failedModules = 0;
|
||||
var mergedImportCount = 0;
|
||||
@@ -619,18 +685,18 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
loadedImages.Add(new LoadedModuleImage(modulePath, moduleImage));
|
||||
loadedModules++;
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
Log.Info(
|
||||
$"Loaded module {Path.GetFileName(modulePath)}: entry=0x{moduleImage.EntryPoint:X16}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
failedModules++;
|
||||
Console.Error.WriteLine($"[RUNTIME] Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})");
|
||||
Log.Error($"Module load failed: {modulePath} ({ex.GetType().Name}: {ex.Message})", ex);
|
||||
}
|
||||
}
|
||||
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
Log.Info(
|
||||
$"Module preload summary: loaded={loadedModules}, failed={failedModules}, merged_imports={mergedImportCount}, merged_symbols={mergedSymbolCount}");
|
||||
return loadedImages;
|
||||
}
|
||||
|
||||
@@ -650,8 +716,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (rebound != 0 || unresolved != 0)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
Log.Info(
|
||||
$"Imported data rebind: rebound={rebound}, unresolved={unresolved}");
|
||||
}
|
||||
}
|
||||
|
||||
@@ -683,8 +749,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
Log.Warning(
|
||||
$"Imported data unresolved: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} addend=0x{unchecked((ulong)relocation.Addend):X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -696,8 +762,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Log.Error(
|
||||
$"Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
unresolved++;
|
||||
@@ -706,8 +772,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
|
||||
if (logRebind)
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
Log.Debug(
|
||||
$"Imported data rebound: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
|
||||
}
|
||||
|
||||
rebound++;
|
||||
@@ -743,8 +809,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
{
|
||||
if (!string.Equals(existingNid, nid, StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
Log.Warning(
|
||||
$"Import stub conflict at 0x{address:X16}: keep={existingNid}, skip={nid} ({Path.GetFileName(modulePath)})");
|
||||
}
|
||||
|
||||
continue;
|
||||
@@ -853,8 +919,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
|
||||
image.EntryPoint,
|
||||
isMain,
|
||||
isSystemModule);
|
||||
Console.Error.WriteLine(
|
||||
$"[RUNTIME] Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||
Log.Info(
|
||||
$"Registered module handle={handle} name={Path.GetFileName(modulePath)} base=0x{baseAddress:X16} size=0x{size:X16}");
|
||||
}
|
||||
|
||||
private static bool TryComputeImageRange(SelfImage image, out ulong baseAddress, out ulong size)
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<!--
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
@@ -14,6 +14,10 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<PackageReference Include="Iced" />
|
||||
</ItemGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
@@ -65,13 +65,17 @@
|
||||
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
|
||||
},
|
||||
"sharpemu.hle": {
|
||||
"type": "Project"
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.Logging": "[1.0.0, )"
|
||||
}
|
||||
},
|
||||
"sharpemu.libs": {
|
||||
"type": "Project",
|
||||
"dependencies": {
|
||||
"SharpEmu.HLE": "[1.0.0, )",
|
||||
"Silk.NET.Vulkan": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
|
||||
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
|
||||
"Silk.NET.Windowing": "[2.23.0, )"
|
||||
}
|
||||
@@ -88,6 +92,16 @@
|
||||
"Silk.NET.Core": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.EXT": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
"resolved": "2.23.0",
|
||||
"contentHash": "+Oth189ksRiL6HvGCwIdnsYHawqrbO8y49u1H61z3wsfcHhQZeVDYe/wF5LD7fk3NcdgDvwFD3mLm1QWhdZySw==",
|
||||
"dependencies": {
|
||||
"Silk.NET.Core": "2.23.0",
|
||||
"Silk.NET.Vulkan": "2.23.0"
|
||||
}
|
||||
},
|
||||
"Silk.NET.Vulkan.Extensions.KHR": {
|
||||
"type": "CentralTransitive",
|
||||
"requested": "[2.23.0, )",
|
||||
|
||||
@@ -0,0 +1,226 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Application xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="SharpEmu.GUI.App"
|
||||
RequestedThemeVariant="Dark">
|
||||
|
||||
<Application.Resources>
|
||||
<Color x:Key="SystemAccentColor">#7C5CFC</Color>
|
||||
|
||||
<LinearGradientBrush x:Key="BgBrush" StartPoint="0%,0%" EndPoint="100%,100%">
|
||||
<GradientStop Offset="0" Color="#12151F" />
|
||||
<GradientStop Offset="0.55" Color="#0D1017" />
|
||||
<GradientStop Offset="1" Color="#0B0D14" />
|
||||
</LinearGradientBrush>
|
||||
|
||||
<SolidColorBrush x:Key="ChromeBrush" Color="#090C12" />
|
||||
<SolidColorBrush x:Key="CardBrush" Color="#141924" />
|
||||
<SolidColorBrush x:Key="CardBorderBrush" Color="#232B3A" />
|
||||
<SolidColorBrush x:Key="ElevatedBrush" Color="#1B2230" />
|
||||
<SolidColorBrush x:Key="TextBrush" Color="#E8ECF4" />
|
||||
<SolidColorBrush x:Key="MutedBrush" Color="#8B94A7" />
|
||||
<SolidColorBrush x:Key="FaintBrush" Color="#5A6478" />
|
||||
<SolidColorBrush x:Key="AccentBrush" Color="#7C5CFC" />
|
||||
<SolidColorBrush x:Key="AccentHoverBrush" Color="#8F73FF" />
|
||||
<SolidColorBrush x:Key="DangerBrush" Color="#E5484D" />
|
||||
<SolidColorBrush x:Key="DangerHoverBrush" Color="#F2555A" />
|
||||
<SolidColorBrush x:Key="SuccessBrush" Color="#46C46B" />
|
||||
<SolidColorBrush x:Key="TileHoverBrush" Color="#1A2130" />
|
||||
<SolidColorBrush x:Key="TileSelectedBrush" Color="#212A3F" />
|
||||
</Application.Resources>
|
||||
|
||||
<Application.Styles>
|
||||
<FluentTheme />
|
||||
|
||||
<Style Selector="Window">
|
||||
<Setter Property="FontFamily" Value="Inter, Segoe UI, sans-serif" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.card">
|
||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="12" />
|
||||
<Setter Property="Padding" Value="16" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.pill">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="CornerRadius" Value="999" />
|
||||
<Setter Property="Padding" Value="10,3" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.sectionTitle">
|
||||
<Setter Property="FontSize" Value="11" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="LetterSpacing" Value="1.5" />
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBlock.fieldLabel">
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
|
||||
<Setter Property="Margin" Value="0,0,0,6" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="TextBox">
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.accent">
|
||||
<Setter Property="Background" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Padding" Value="22,10" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.accent:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource AccentHoverBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.danger">
|
||||
<Setter Property="Background" Value="{StaticResource DangerBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
<Setter Property="FontWeight" Value="SemiBold" />
|
||||
<Setter Property="Padding" Value="22,10" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.danger:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource DangerHoverBrush}" />
|
||||
<Setter Property="Foreground" Value="White" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Button.ghost">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="Padding" Value="12,7" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ToggleButton.ghost">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
<Setter Property="Padding" Value="12,7" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="ToggleButton.ghost:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="ToggleButton.ghost:checked /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Top-level page switcher (Library / Options): plain transparent
|
||||
buttons, not TabItem, so there is no Fluent selected-tab underline.
|
||||
The active page is conveyed by brightness alone; LB/RB gamepad
|
||||
hints flank the pair. -->
|
||||
<Style Selector="Button.segment">
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="0" />
|
||||
<Setter Property="Foreground" Value="{StaticResource MutedBrush}" />
|
||||
<Setter Property="FontSize" Value="22" />
|
||||
<Setter Property="FontWeight" Value="Bold" />
|
||||
<Setter Property="Padding" Value="6,4" />
|
||||
<Setter Property="CornerRadius" Value="8" />
|
||||
</Style>
|
||||
<Style Selector="Button.segment:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
<Style Selector="Button.segment.active">
|
||||
<Setter Property="Foreground" Value="{StaticResource TextBrush}" />
|
||||
</Style>
|
||||
|
||||
<!-- Gamepad shoulder-button hint chip (LB/RB, L1/R1). -->
|
||||
<Style Selector="Border.padHint">
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
<Setter Property="CornerRadius" Value="6" />
|
||||
<Setter Property="Padding" Value="8,3" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ContextMenu">
|
||||
<Setter Property="Background" Value="{StaticResource CardBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="Padding" Value="6" />
|
||||
</Style>
|
||||
<Style Selector="ContextMenu MenuItem">
|
||||
<Setter Property="Padding" Value="10,7" />
|
||||
<Setter Property="CornerRadius" Value="7" />
|
||||
</Style>
|
||||
<Style Selector="ContextMenu Separator">
|
||||
<Setter Property="Background" Value="{StaticResource CardBorderBrush}" />
|
||||
<Setter Property="Height" Value="1" />
|
||||
<Setter Property="Margin" Value="8,4" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="ListBox.console">
|
||||
<Setter Property="Background" Value="#0B0E14" />
|
||||
<Setter Property="FontFamily" Value="Cascadia Mono, Consolas, Courier New, monospace" />
|
||||
<Setter Property="FontSize" Value="12" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.console ListBoxItem">
|
||||
<Setter Property="Padding" Value="10,1" />
|
||||
<Setter Property="MinHeight" Value="0" />
|
||||
</Style>
|
||||
|
||||
<!-- Cover-art library grid -->
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem">
|
||||
<Setter Property="Padding" Value="10" />
|
||||
<Setter Property="Margin" Value="5" />
|
||||
<Setter Property="CornerRadius" Value="14" />
|
||||
<Setter Property="Background" Value="Transparent" />
|
||||
<Setter Property="BorderThickness" Value="1" />
|
||||
<Setter Property="BorderBrush" Value="Transparent" />
|
||||
<Setter Property="RenderTransform" Value="translateY(0px)" />
|
||||
<Setter Property="Transitions">
|
||||
<Transitions>
|
||||
<TransformOperationsTransition Property="RenderTransform" Duration="0:0:0.12" />
|
||||
</Transitions>
|
||||
</Setter>
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover">
|
||||
<Setter Property="RenderTransform" Value="translateY(-3px)" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource TileHoverBrush}" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:selected /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentBrush}" />
|
||||
</Style>
|
||||
<Style Selector="ListBox.tileGrid ListBoxItem:selected:pointerover /template/ ContentPresenter#PART_ContentPresenter">
|
||||
<Setter Property="Background" Value="{StaticResource TileSelectedBrush}" />
|
||||
<Setter Property="BorderBrush" Value="{StaticResource AccentHoverBrush}" />
|
||||
</Style>
|
||||
|
||||
<Style Selector="Border.coverShadow">
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="BoxShadow" Value="0 6 14 0 #55000000" />
|
||||
</Style>
|
||||
<Style Selector="Border.coverClip">
|
||||
<Setter Property="CornerRadius" Value="10" />
|
||||
<Setter Property="ClipToBounds" Value="True" />
|
||||
<Setter Property="Background" Value="{StaticResource ElevatedBrush}" />
|
||||
</Style>
|
||||
</Application.Styles>
|
||||
|
||||
</Application>
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Avalonia;
|
||||
using Avalonia.Controls.ApplicationLifetimes;
|
||||
using Avalonia.Markup.Xaml;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public partial class App : Application
|
||||
{
|
||||
public override void Initialize()
|
||||
{
|
||||
AvaloniaXamlLoader.Load(this);
|
||||
}
|
||||
|
||||
public override void OnFrameworkInitializationCompleted()
|
||||
{
|
||||
if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
|
||||
{
|
||||
desktop.MainWindow = new MainWindow();
|
||||
}
|
||||
|
||||
base.OnFrameworkInitializationCompleted();
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,120 @@
|
||||
#nullable disable
|
||||
using System.IO;
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
/// <summary>
|
||||
/// Stores the configuration data needed to decode or encode an ATRAC9 stream.
|
||||
/// </summary>
|
||||
public class Atrac9Config
|
||||
{
|
||||
/// <summary>
|
||||
/// The 4-byte ATRAC9 configuration data.
|
||||
/// </summary>
|
||||
public byte[] ConfigData { get; }
|
||||
|
||||
/// <summary>
|
||||
/// A 4-bit value specifying one of 16 sample rates.
|
||||
/// </summary>
|
||||
public int SampleRateIndex { get; }
|
||||
/// <summary>
|
||||
/// A 3-bit value specifying one of 6 substream channel mappings.
|
||||
/// </summary>
|
||||
public int ChannelConfigIndex { get; }
|
||||
/// <summary>
|
||||
/// An 11-bit value containing the average size of a single frame.
|
||||
/// </summary>
|
||||
public int FrameBytes { get; }
|
||||
/// <summary>
|
||||
/// A 2-bit value indicating how many frames are in each superframe.
|
||||
/// </summary>
|
||||
public int SuperframeIndex { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The channel mapping used by the ATRAC9 stream.
|
||||
/// </summary>
|
||||
public ChannelConfig ChannelConfig { get; }
|
||||
/// <summary>
|
||||
/// The total number of channels in the ATRAC9 stream.
|
||||
/// </summary>
|
||||
public int ChannelCount { get; }
|
||||
/// <summary>
|
||||
/// The sample rate of the ATRAC9 stream.
|
||||
/// </summary>
|
||||
public int SampleRate { get; }
|
||||
/// <summary>
|
||||
/// Indicates whether the ATRAC9 stream has a <see cref="SampleRateIndex"/> of 8 or above.
|
||||
/// </summary>
|
||||
public bool HighSampleRate { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of frames in each superframe.
|
||||
/// </summary>
|
||||
public int FramesPerSuperframe { get; }
|
||||
/// <summary>
|
||||
/// The number of samples in one frame as an exponent of 2.
|
||||
/// <see cref="FrameSamples"/> = 2^<see cref="FrameSamplesPower"/>.
|
||||
/// </summary>
|
||||
public int FrameSamplesPower { get; }
|
||||
/// <summary>
|
||||
/// The number of samples in one frame.
|
||||
/// </summary>
|
||||
public int FrameSamples { get; }
|
||||
/// <summary>
|
||||
/// The number of bytes in one superframe.
|
||||
/// </summary>
|
||||
public int SuperframeBytes { get; }
|
||||
/// <summary>
|
||||
/// The number of samples in one superframe.
|
||||
/// </summary>
|
||||
public int SuperframeSamples { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Reads ATRAC9 configuration data and calculates the stream parameters from it.
|
||||
/// </summary>
|
||||
/// <param name="configData">The processed ATRAC9 configuration.</param>
|
||||
public Atrac9Config(byte[] configData)
|
||||
{
|
||||
if (configData == null || configData.Length != 4)
|
||||
{
|
||||
throw new InvalidDataException("Config data must be 4 bytes long");
|
||||
}
|
||||
|
||||
ReadConfigData(configData, out int a, out int b, out int c, out int d);
|
||||
SampleRateIndex = a;
|
||||
ChannelConfigIndex = b;
|
||||
FrameBytes = c;
|
||||
SuperframeIndex = d;
|
||||
ConfigData = configData;
|
||||
|
||||
FramesPerSuperframe = 1 << SuperframeIndex;
|
||||
SuperframeBytes = FrameBytes << SuperframeIndex;
|
||||
ChannelConfig = Tables.ChannelConfig[ChannelConfigIndex];
|
||||
|
||||
ChannelCount = ChannelConfig.ChannelCount;
|
||||
SampleRate = Tables.SampleRates[SampleRateIndex];
|
||||
HighSampleRate = SampleRateIndex > 7;
|
||||
FrameSamplesPower = Tables.SamplingRateIndexToFrameSamplesPower[SampleRateIndex];
|
||||
FrameSamples = 1 << FrameSamplesPower;
|
||||
SuperframeSamples = FrameSamples * FramesPerSuperframe;
|
||||
}
|
||||
|
||||
private static void ReadConfigData(byte[] configData, out int sampleRateIndex, out int channelConfigIndex, out int frameBytes, out int superframeIndex)
|
||||
{
|
||||
var reader = new BitReader(configData);
|
||||
|
||||
int header = reader.ReadInt(8);
|
||||
sampleRateIndex = reader.ReadInt(4);
|
||||
channelConfigIndex = reader.ReadInt(3);
|
||||
int validationBit = reader.ReadInt(1);
|
||||
frameBytes = reader.ReadInt(11) + 1;
|
||||
superframeIndex = reader.ReadInt(2);
|
||||
|
||||
if (header != 0xFE || validationBit != 0)
|
||||
{
|
||||
throw new InvalidDataException("ATRAC9 Config Data is invalid");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
/// <summary>
|
||||
/// Decodes an ATRAC9 stream into 16-bit PCM.
|
||||
/// </summary>
|
||||
public class Atrac9Decoder
|
||||
{
|
||||
/// <summary>
|
||||
/// The config data for the current ATRAC9 stream.
|
||||
/// </summary>
|
||||
public Atrac9Config Config { get; private set; }
|
||||
|
||||
private Frame Frame { get; set; }
|
||||
private BitReader Reader { get; set; }
|
||||
private bool _initialized;
|
||||
|
||||
/// <summary>
|
||||
/// Sets up the decoder to decode an ATRAC9 stream based on the information in <paramref name="configData"/>.
|
||||
/// </summary>
|
||||
/// <param name="configData">A 4-byte value containing information about the ATRAC9 stream.</param>
|
||||
public void Initialize(byte[] configData)
|
||||
{
|
||||
Config = new Atrac9Config(configData);
|
||||
Frame = new Frame(Config);
|
||||
Reader = new BitReader(null);
|
||||
_initialized = true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Decodes one superframe of ATRAC9 data.
|
||||
/// </summary>
|
||||
/// <param name="atrac9Data">The ATRAC9 data to decode. The array must be at least
|
||||
/// <see cref="Config"/>.<see cref="Atrac9Config.SuperframeBytes"/> bytes long.</param>
|
||||
/// <param name="pcmOut">A buffer that the decoded PCM data will be placed in.
|
||||
/// The array must have dimensions of at least [<see cref="Config"/>.<see cref="Atrac9Config.ChannelCount"/>]
|
||||
/// [<see cref="Config"/>.<see cref="Atrac9Config.SuperframeSamples"/>].</param>
|
||||
public void Decode(byte[] atrac9Data, short[][] pcmOut)
|
||||
{
|
||||
if (!_initialized) throw new InvalidOperationException("Decoder must be initialized before decoding.");
|
||||
|
||||
ValidateDecodeBuffers(atrac9Data, pcmOut);
|
||||
Reader.SetBuffer(atrac9Data);
|
||||
DecodeSuperFrame(pcmOut);
|
||||
}
|
||||
|
||||
private void ValidateDecodeBuffers(byte[] atrac9Buffer, short[][] pcmBuffer)
|
||||
{
|
||||
if (atrac9Buffer == null) throw new ArgumentNullException(nameof(atrac9Buffer));
|
||||
if (pcmBuffer == null) throw new ArgumentNullException(nameof(pcmBuffer));
|
||||
|
||||
if (atrac9Buffer.Length < Config.SuperframeBytes)
|
||||
{
|
||||
throw new ArgumentException("ATRAC9 buffer is too small");
|
||||
}
|
||||
|
||||
if (pcmBuffer.Length < Config.ChannelCount)
|
||||
{
|
||||
throw new ArgumentException("PCM buffer is too small");
|
||||
}
|
||||
|
||||
for (int i = 0; i < Config.ChannelCount; i++)
|
||||
{
|
||||
if (pcmBuffer[i]?.Length < Config.SuperframeSamples)
|
||||
{
|
||||
throw new ArgumentException("PCM buffer is too small");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void DecodeSuperFrame(short[][] pcmOut)
|
||||
{
|
||||
for (int i = 0; i < Config.FramesPerSuperframe; i++)
|
||||
{
|
||||
Frame.FrameIndex = i;
|
||||
DecodeFrame(Reader, Frame);
|
||||
PcmFloatToShort(pcmOut, i * Config.FrameSamples);
|
||||
Reader.AlignPosition(8);
|
||||
}
|
||||
}
|
||||
|
||||
private void PcmFloatToShort(short[][] pcmOut, int start)
|
||||
{
|
||||
int endSample = start + Config.FrameSamples;
|
||||
int channelNum = 0;
|
||||
foreach (Block block in Frame.Blocks)
|
||||
{
|
||||
foreach (Channel channel in block.Channels)
|
||||
{
|
||||
double[] pcmSrc = channel.Pcm;
|
||||
short[] pcmDest = pcmOut[channelNum++];
|
||||
for (int d = 0, s = start; s < endSample; d++, s++)
|
||||
{
|
||||
double sample = pcmSrc[d];
|
||||
// Not using Math.Round because it's ~20x slower on 64-bit
|
||||
int roundedSample = (int)Math.Floor(sample + 0.5);
|
||||
pcmDest[s] = Helpers.Clamp16(roundedSample);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DecodeFrame(BitReader reader, Frame frame)
|
||||
{
|
||||
Unpack.UnpackFrame(reader, frame);
|
||||
|
||||
foreach (Block block in frame.Blocks)
|
||||
{
|
||||
Quantization.DequantizeSpectra(block);
|
||||
Stereo.ApplyIntensityStereo(block);
|
||||
Quantization.ScaleSpectrum(block);
|
||||
BandExtension.ApplyBandExtension(block);
|
||||
ImdctBlock(block);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ImdctBlock(Block block)
|
||||
{
|
||||
foreach (Channel channel in block.Channels)
|
||||
{
|
||||
channel.Mdct.RunImdct(channel.Spectra, channel.Pcm);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9
|
||||
{
|
||||
/// <summary>
|
||||
/// An Xorshift RNG used by the ATRAC9 codec
|
||||
/// </summary>
|
||||
internal class Atrac9Rng
|
||||
{
|
||||
private ushort _stateA;
|
||||
private ushort _stateB;
|
||||
private ushort _stateC;
|
||||
private ushort _stateD;
|
||||
|
||||
public Atrac9Rng(ushort seed)
|
||||
{
|
||||
int startValue = 0x4D93 * (seed ^ (seed >> 14));
|
||||
|
||||
_stateA = (ushort)(3 - startValue);
|
||||
_stateB = (ushort)(2 - startValue);
|
||||
_stateC = (ushort)(1 - startValue);
|
||||
_stateD = (ushort)(0 - startValue);
|
||||
}
|
||||
|
||||
public ushort Next()
|
||||
{
|
||||
ushort t = (ushort)(_stateD ^ (_stateD << 5));
|
||||
_stateD = _stateC;
|
||||
_stateC = _stateB;
|
||||
_stateB = _stateA;
|
||||
_stateA = (ushort)(t ^ _stateA ^ ((t ^ (_stateA >> 5)) >> 4));
|
||||
return _stateA;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,373 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class BandExtension
|
||||
{
|
||||
public static void ApplyBandExtension(Block block)
|
||||
{
|
||||
if (!block.BandExtensionEnabled || !block.HasExtensionData) return;
|
||||
|
||||
foreach (Channel channel in block.Channels)
|
||||
{
|
||||
ApplyBandExtensionChannel(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ApplyBandExtensionChannel(Channel channel)
|
||||
{
|
||||
int groupAUnit = channel.Block.QuantizationUnitCount;
|
||||
int[] scaleFactors = channel.ScaleFactors;
|
||||
double[] spectra = channel.Spectra;
|
||||
double[] scales = channel.BexScales;
|
||||
int[] values = channel.BexValues;
|
||||
|
||||
GetBexBandInfo(out int bandCount, out int groupBUnit, out int groupCUnit, groupAUnit);
|
||||
int totalUnits = Math.Max(groupCUnit, 22);
|
||||
|
||||
int groupABin = Tables.QuantUnitToCoeffIndex[groupAUnit];
|
||||
int groupBBin = Tables.QuantUnitToCoeffIndex[groupBUnit];
|
||||
int groupCBin = Tables.QuantUnitToCoeffIndex[groupCUnit];
|
||||
int totalBins = Tables.QuantUnitToCoeffIndex[totalUnits];
|
||||
|
||||
FillHighFrequencies(spectra, groupABin, groupBBin, groupCBin, totalBins);
|
||||
|
||||
switch (channel.BexMode)
|
||||
{
|
||||
case 0:
|
||||
int bexQuantUnits = totalUnits - groupAUnit;
|
||||
|
||||
switch (bandCount)
|
||||
{
|
||||
case 3:
|
||||
scales[0] = BexMode0Bands3[0][values[0]];
|
||||
scales[1] = BexMode0Bands3[1][values[0]];
|
||||
scales[2] = BexMode0Bands3[2][values[1]];
|
||||
scales[3] = BexMode0Bands3[3][values[2]];
|
||||
scales[4] = BexMode0Bands3[4][values[3]];
|
||||
break;
|
||||
case 4:
|
||||
scales[0] = BexMode0Bands4[0][values[0]];
|
||||
scales[1] = BexMode0Bands4[1][values[0]];
|
||||
scales[2] = BexMode0Bands4[2][values[1]];
|
||||
scales[3] = BexMode0Bands4[3][values[2]];
|
||||
scales[4] = BexMode0Bands4[4][values[3]];
|
||||
break;
|
||||
case 5:
|
||||
scales[0] = BexMode0Bands5[0][values[0]];
|
||||
scales[1] = BexMode0Bands5[1][values[1]];
|
||||
scales[2] = BexMode0Bands5[2][values[1]];
|
||||
break;
|
||||
}
|
||||
|
||||
scales[bexQuantUnits - 1] = Tables.SpectrumScale[scaleFactors[groupAUnit]];
|
||||
|
||||
AddNoiseToSpectrum(channel, Tables.QuantUnitToCoeffIndex[totalUnits - 1],
|
||||
Tables.QuantUnitToCoeffCount[totalUnits - 1]);
|
||||
ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits);
|
||||
break;
|
||||
case 1:
|
||||
for (int i = groupAUnit; i < totalUnits; i++)
|
||||
{
|
||||
scales[i - groupAUnit] = Tables.SpectrumScale[scaleFactors[i]];
|
||||
}
|
||||
|
||||
AddNoiseToSpectrum(channel, groupABin, totalBins - groupABin);
|
||||
ScaleBexQuantUnits(spectra, scales, groupAUnit, totalUnits);
|
||||
break;
|
||||
case 2:
|
||||
double groupAScale2 = BexMode2Scale[values[0]];
|
||||
double groupBScale2 = BexMode2Scale[values[1]];
|
||||
|
||||
for (int i = groupABin; i < groupBBin; i++)
|
||||
{
|
||||
spectra[i] *= groupAScale2;
|
||||
}
|
||||
|
||||
for (int i = groupBBin; i < groupCBin; i++)
|
||||
{
|
||||
spectra[i] *= groupBScale2;
|
||||
}
|
||||
return;
|
||||
case 3:
|
||||
double rate = Math.Pow(2, BexMode3Rate[values[1]]);
|
||||
double scale = BexMode3Initial[values[0]];
|
||||
for (int i = groupABin; i < totalBins; i++)
|
||||
{
|
||||
scale *= rate;
|
||||
spectra[i] *= scale;
|
||||
}
|
||||
return;
|
||||
case 4:
|
||||
double mult = BexMode4Multiplier[values[0]];
|
||||
double groupAScale4 = 0.7079468 * mult;
|
||||
double groupBScale4 = 0.5011902 * mult;
|
||||
double groupCScale4 = 0.3548279 * mult;
|
||||
|
||||
for (int i = groupABin; i < groupBBin; i++)
|
||||
{
|
||||
spectra[i] *= groupAScale4;
|
||||
}
|
||||
|
||||
for (int i = groupBBin; i < groupCBin; i++)
|
||||
{
|
||||
spectra[i] *= groupBScale4;
|
||||
}
|
||||
|
||||
for (int i = groupCBin; i < totalBins; i++)
|
||||
{
|
||||
spectra[i] *= groupCScale4;
|
||||
}
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ScaleBexQuantUnits(double[] spectra, double[] scales, int startUnit, int totalUnits)
|
||||
{
|
||||
for (int i = startUnit; i < totalUnits; i++)
|
||||
{
|
||||
for (int k = Tables.QuantUnitToCoeffIndex[i]; k < Tables.QuantUnitToCoeffIndex[i + 1]; k++)
|
||||
{
|
||||
spectra[k] *= scales[i - startUnit];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void FillHighFrequencies(double[] spectra, int groupABin, int groupBBin, int groupCBin, int totalBins)
|
||||
{
|
||||
for (int i = 0; i < groupBBin - groupABin; i++)
|
||||
{
|
||||
spectra[groupABin + i] = spectra[groupABin - i - 1];
|
||||
}
|
||||
|
||||
for (int i = 0; i < groupCBin - groupBBin; i++)
|
||||
{
|
||||
spectra[groupBBin + i] = spectra[groupBBin - i - 1];
|
||||
}
|
||||
|
||||
for (int i = 0; i < totalBins - groupCBin; i++)
|
||||
{
|
||||
spectra[groupCBin + i] = spectra[groupCBin - i - 1];
|
||||
}
|
||||
}
|
||||
|
||||
private static void AddNoiseToSpectrum(Channel channel, int index, int count)
|
||||
{
|
||||
if (channel.Rng == null)
|
||||
{
|
||||
int[] sf = channel.ScaleFactors;
|
||||
ushort seed = (ushort)(543 * (sf[8] + sf[12] + sf[15] + 1));
|
||||
channel.Rng = new Atrac9Rng(seed);
|
||||
}
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
channel.Spectra[i + index] = channel.Rng.Next() / 65535.0 * 2.0 - 1.0;
|
||||
}
|
||||
}
|
||||
|
||||
public static void GetBexBandInfo(out int bandCount, out int groupAUnit, out int groupBUnit, int quantUnits)
|
||||
{
|
||||
groupAUnit = BexGroupInfo[quantUnits - 13][0];
|
||||
groupBUnit = BexGroupInfo[quantUnits - 13][1];
|
||||
bandCount = BexGroupInfo[quantUnits - 13][2];
|
||||
}
|
||||
|
||||
public static readonly byte[][] BexGroupInfo =
|
||||
{
|
||||
new byte[] {16, 21, 0},
|
||||
new byte[] {18, 22, 1},
|
||||
new byte[] {20, 22, 2},
|
||||
new byte[] {21, 22, 3},
|
||||
new byte[] {21, 22, 3},
|
||||
new byte[] {23, 24, 4},
|
||||
new byte[] {23, 24, 4},
|
||||
new byte[] {24, 24, 5}
|
||||
};
|
||||
|
||||
// [mode][bands]
|
||||
|
||||
public static readonly byte[][] BexEncodedValueCounts =
|
||||
{
|
||||
new byte[] {0, 0, 0, 4, 4, 2},
|
||||
new byte[] {0, 0, 0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 2, 2, 1},
|
||||
new byte[] {0, 0, 0, 2, 2, 2},
|
||||
new byte[] {1, 1, 1, 0, 0, 0}
|
||||
};
|
||||
|
||||
// [mode][bands][valueIndex]
|
||||
|
||||
public static readonly byte[][][] BexDataLengths =
|
||||
{
|
||||
new[] {
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {5, 4, 3, 3},
|
||||
new byte[] {4, 4, 3, 4},
|
||||
new byte[] {4, 5, 0, 0}
|
||||
}, new[] {
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0}
|
||||
}, new[] {
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {6, 6, 0, 0},
|
||||
new byte[] {6, 6, 0, 0},
|
||||
new byte[] {6, 0, 0, 0}
|
||||
}, new[] {
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {4, 4, 0, 0},
|
||||
new byte[] {4, 4, 0, 0},
|
||||
new byte[] {4, 4, 0, 0}
|
||||
}, new[] {
|
||||
new byte[] {3, 0, 0, 0},
|
||||
new byte[] {3, 0, 0, 0},
|
||||
new byte[] {3, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0},
|
||||
new byte[] {0, 0, 0, 0}
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly double[][] BexMode0Bands3 =
|
||||
{
|
||||
new[] {
|
||||
0.000000e+0, 1.988220e-1, 2.514343e-1, 2.960510e-1,
|
||||
3.263550e-1, 3.771362e-1, 3.786926e-1, 4.540405e-1,
|
||||
4.877625e-1, 5.262451e-1, 5.447083e-1, 5.737000e-1,
|
||||
6.212158e-1, 6.222839e-1, 6.560974e-1, 6.896667e-1,
|
||||
7.555542e-1, 7.677917e-1, 7.918091e-1, 7.971497e-1,
|
||||
8.188171e-1, 8.446045e-1, 9.790649e-1, 9.822083e-1,
|
||||
9.846191e-1, 9.859314e-1, 9.863586e-1, 9.863892e-1,
|
||||
9.873352e-1, 9.881287e-1, 9.898682e-1, 9.913330e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 9.982910e-1, 7.592773e-2, 7.179565e-1,
|
||||
9.851379e-1, 5.340271e-1, 9.013672e-1, 6.349182e-1,
|
||||
7.226257e-1, 1.948547e-1, 7.628174e-1, 9.873657e-1,
|
||||
8.112183e-1, 2.715454e-1, 9.734192e-1, 1.443787e-1,
|
||||
4.640198e-1, 3.249207e-1, 3.790894e-1, 8.276367e-2,
|
||||
5.954590e-1, 2.864380e-1, 9.806824e-1, 7.929077e-1,
|
||||
6.292114e-1, 4.887085e-1, 2.905273e-1, 1.301880e-1,
|
||||
3.140869e-1, 5.482483e-1, 4.210815e-1, 1.182861e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 3.155518e-2, 8.581543e-2, 1.364746e-1,
|
||||
1.858826e-1, 2.368469e-1, 2.888184e-1, 3.432617e-1,
|
||||
4.012451e-1, 4.623108e-1, 5.271301e-1, 5.954895e-1,
|
||||
6.681213e-1, 7.448425e-1, 8.245239e-1, 9.097290e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 4.418945e-2, 1.303711e-1, 2.273560e-1,
|
||||
3.395996e-1, 4.735718e-1, 6.267090e-1, 8.003845e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 2.804565e-2, 9.683228e-2, 1.849976e-1,
|
||||
3.005981e-1, 4.470520e-1, 6.168518e-1, 8.007813e-1
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly double[][] BexMode0Bands4 =
|
||||
{
|
||||
new[] {
|
||||
0.000000e+0, 2.708740e-1, 3.479614e-1, 3.578186e-1,
|
||||
5.083618e-1, 5.299072e-1, 5.819092e-1, 6.381836e-1,
|
||||
7.276917e-1, 7.595520e-1, 7.878723e-1, 9.707336e-1,
|
||||
9.713135e-1, 9.736023e-1, 9.759827e-1, 9.832458e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 2.330627e-1, 5.891418e-1, 7.170410e-1,
|
||||
2.036438e-1, 1.613464e-1, 6.668701e-1, 9.481201e-1,
|
||||
9.769897e-1, 5.111694e-1, 3.522644e-1, 8.209534e-1,
|
||||
2.933960e-1, 9.757690e-1, 5.289917e-1, 4.372253e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 4.360962e-2, 1.056519e-1, 1.590576e-1,
|
||||
2.078857e-1, 2.572937e-1, 3.082581e-1, 3.616028e-1,
|
||||
4.191589e-1, 4.792175e-1, 5.438538e-1, 6.125183e-1,
|
||||
6.841125e-1, 7.589417e-1, 8.365173e-1, 9.148254e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 4.074097e-2, 1.164551e-1, 2.077026e-1,
|
||||
3.184509e-1, 4.532166e-1, 6.124268e-1, 7.932129e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 8.880615e-3, 2.932739e-2, 5.593872e-2,
|
||||
8.825684e-2, 1.259155e-1, 1.721497e-1, 2.270813e-1,
|
||||
2.901611e-1, 3.579712e-1, 4.334106e-1, 5.147095e-1,
|
||||
6.023254e-1, 6.956177e-1, 7.952881e-1, 8.977356e-1
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly double[][] BexMode0Bands5 =
|
||||
{
|
||||
new[] {
|
||||
0.000000e+0, 7.379150e-2, 1.806335e-1, 2.687073e-1,
|
||||
3.407898e-1, 4.047546e-1, 4.621887e-1, 5.168762e-1,
|
||||
5.703125e-1, 6.237488e-1, 6.763611e-1, 7.288208e-1,
|
||||
7.808533e-1, 8.337708e-1, 8.874512e-1, 9.418030e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 7.980347e-2, 1.615295e-1, 1.665649e-1,
|
||||
1.822205e-1, 2.185669e-1, 2.292175e-1, 2.456665e-1,
|
||||
2.666321e-1, 3.306580e-1, 3.330688e-1, 3.765259e-1,
|
||||
4.085083e-1, 4.400024e-1, 4.407654e-1, 4.817505e-1,
|
||||
4.924011e-1, 5.320740e-1, 5.893860e-1, 6.131287e-1,
|
||||
6.212463e-1, 6.278076e-1, 6.308899e-1, 7.660828e-1,
|
||||
7.850647e-1, 7.910461e-1, 7.929382e-1, 8.038330e-1,
|
||||
9.834900e-1, 9.846191e-1, 9.852295e-1, 9.862671e-1
|
||||
}, new[] {
|
||||
0.000000e+0, 6.084290e-1, 3.672791e-1, 3.151855e-1,
|
||||
1.488953e-1, 2.571716e-1, 5.103455e-1, 3.311157e-1,
|
||||
5.426025e-2, 4.254456e-1, 7.998352e-1, 7.873230e-1,
|
||||
5.418701e-1, 2.925110e-1, 8.468628e-2, 1.410522e-1,
|
||||
9.819641e-1, 9.609070e-1, 3.530884e-2, 9.729004e-2,
|
||||
5.758362e-1, 9.941711e-1, 7.215576e-1, 7.183228e-1,
|
||||
2.028809e-1, 9.588623e-2, 2.032166e-1, 1.338806e-1,
|
||||
5.003357e-1, 1.874390e-1, 9.804993e-1, 1.107788e-1
|
||||
}
|
||||
};
|
||||
|
||||
public static readonly double[] BexMode2Scale =
|
||||
{
|
||||
4.272461e-4, 1.312256e-3, 2.441406e-3, 3.692627e-3,
|
||||
4.913330e-3, 6.134033e-3, 7.507324e-3, 8.972168e-3,
|
||||
1.049805e-2, 1.223755e-2, 1.406860e-2, 1.599121e-2,
|
||||
1.800537e-2, 2.026367e-2, 2.264404e-2, 2.517700e-2,
|
||||
2.792358e-2, 3.073120e-2, 3.344727e-2, 3.631592e-2,
|
||||
3.952026e-2, 4.275513e-2, 4.608154e-2, 4.968262e-2,
|
||||
5.355835e-2, 5.783081e-2, 6.195068e-2, 6.677246e-2,
|
||||
7.196045e-2, 7.745361e-2, 8.319092e-2, 8.993530e-2,
|
||||
9.759521e-2, 1.056213e-1, 1.138916e-1, 1.236267e-1,
|
||||
1.348267e-1, 1.470337e-1, 1.603394e-1, 1.755676e-1,
|
||||
1.905823e-1, 2.071228e-1, 2.245178e-1, 2.444153e-1,
|
||||
2.658997e-1, 2.897644e-1, 3.146057e-1, 3.450012e-1,
|
||||
3.766174e-1, 4.122620e-1, 4.505615e-1, 4.893799e-1,
|
||||
5.305481e-1, 5.731201e-1, 6.157837e-1, 6.580811e-1,
|
||||
6.985168e-1, 7.435303e-1, 7.865906e-1, 8.302612e-1,
|
||||
8.718567e-1, 9.125671e-1, 9.575806e-1, 9.996643e-1
|
||||
};
|
||||
|
||||
public static readonly double[] BexMode3Initial =
|
||||
{
|
||||
3.491211e-1, 5.371094e-1, 6.782227e-1, 7.910156e-1,
|
||||
9.057617e-1, 1.024902e+0, 1.156250e+0, 1.290527e+0,
|
||||
1.458984e+0, 1.664551e+0, 1.929688e+0, 2.278320e+0,
|
||||
2.831543e+0, 3.659180e+0, 5.257813e+0, 8.373047e+0
|
||||
};
|
||||
|
||||
public static readonly double[] BexMode3Rate =
|
||||
{
|
||||
-2.913818e-1, -2.541504e-1, -1.664429e-1, -1.476440e-1,
|
||||
-1.342163e-1, -1.220703e-1, -1.117554e-1, -1.026611e-1,
|
||||
-9.436035e-2, -8.483887e-2, -7.476807e-2, -6.304932e-2,
|
||||
-4.492188e-2, -2.447510e-2, +1.831055e-4, +4.174805e-2
|
||||
};
|
||||
|
||||
public static readonly double[] BexMode4Multiplier =
|
||||
{
|
||||
3.610229e-2, 1.260681e-1, 2.227478e-1, 3.338318e-1,
|
||||
4.662170e-1, 6.221313e-1, 7.989197e-1, 9.939575e-1
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,141 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class BitAllocation
|
||||
{
|
||||
public static void CreateGradient(Block block)
|
||||
{
|
||||
int valueCount = block.GradientEndValue - block.GradientStartValue;
|
||||
int unitCount = block.GradientEndUnit - block.GradientStartUnit;
|
||||
|
||||
for (int i = 0; i < block.GradientEndUnit; i++)
|
||||
{
|
||||
block.Gradient[i] = block.GradientStartValue;
|
||||
}
|
||||
|
||||
for (int i = block.GradientEndUnit; i <= block.QuantizationUnitCount; i++)
|
||||
{
|
||||
block.Gradient[i] = block.GradientEndValue;
|
||||
}
|
||||
if (unitCount <= 0) return;
|
||||
if (valueCount == 0) return;
|
||||
|
||||
byte[] curve = Tables.GradientCurves[unitCount - 1];
|
||||
if (valueCount <= 0)
|
||||
{
|
||||
double scale = (-valueCount - 1) / 31.0;
|
||||
int baseVal = block.GradientStartValue - 1;
|
||||
for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++)
|
||||
{
|
||||
block.Gradient[i] = baseVal - (int)(curve[i - block.GradientStartUnit] * scale);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
double scale = (valueCount - 1) / 31.0;
|
||||
int baseVal = block.GradientStartValue + 1;
|
||||
for (int i = block.GradientStartUnit; i < block.GradientEndUnit; i++)
|
||||
{
|
||||
block.Gradient[i] = baseVal + (int)(curve[i - block.GradientStartUnit] * scale);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CalculateMask(Channel channel)
|
||||
{
|
||||
Array.Clear(channel.PrecisionMask, 0, channel.PrecisionMask.Length);
|
||||
for (int i = 1; i < channel.Block.QuantizationUnitCount; i++)
|
||||
{
|
||||
int delta = channel.ScaleFactors[i] - channel.ScaleFactors[i - 1];
|
||||
if (delta > 1)
|
||||
{
|
||||
channel.PrecisionMask[i] += Math.Min(delta - 1, 5);
|
||||
}
|
||||
else if (delta < -1)
|
||||
{
|
||||
channel.PrecisionMask[i - 1] += Math.Min(delta * -1 - 1, 5);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static void CalculatePrecisions(Channel channel)
|
||||
{
|
||||
Block block = channel.Block;
|
||||
|
||||
if (block.GradientMode != 0)
|
||||
{
|
||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
channel.Precisions[i] = channel.ScaleFactors[i] + channel.PrecisionMask[i] - block.Gradient[i];
|
||||
if (channel.Precisions[i] > 0)
|
||||
{
|
||||
switch (block.GradientMode)
|
||||
{
|
||||
case 1:
|
||||
channel.Precisions[i] /= 2;
|
||||
break;
|
||||
case 2:
|
||||
channel.Precisions[i] = 3 * channel.Precisions[i] / 8;
|
||||
break;
|
||||
case 3:
|
||||
channel.Precisions[i] /= 4;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
channel.Precisions[i] = channel.ScaleFactors[i] - block.Gradient[i];
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
if (channel.Precisions[i] < 1)
|
||||
{
|
||||
channel.Precisions[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < block.GradientBoundary; i++)
|
||||
{
|
||||
channel.Precisions[i]++;
|
||||
}
|
||||
|
||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
channel.PrecisionsFine[i] = 0;
|
||||
if (channel.Precisions[i] > 15)
|
||||
{
|
||||
channel.PrecisionsFine[i] = channel.Precisions[i] - 15;
|
||||
channel.Precisions[i] = 15;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static byte[][] GenerateGradientCurves()
|
||||
{
|
||||
byte[] main =
|
||||
{
|
||||
01, 01, 01, 01, 02, 02, 02, 02, 03, 03, 03, 04, 04, 05, 05, 06, 07, 08, 09, 10, 11, 12, 13, 15,
|
||||
16, 18, 19, 20, 21, 22, 23, 24, 25, 26, 26, 27, 27, 28, 28, 28, 29, 29, 29, 29, 30, 30, 30, 30
|
||||
};
|
||||
var curves = new byte[main.Length][];
|
||||
|
||||
for (int length = 1; length <= main.Length; length++)
|
||||
{
|
||||
curves[length - 1] = new byte[length];
|
||||
for (int i = 0; i < length; i++)
|
||||
{
|
||||
curves[length - 1][i] = main[i * main.Length / length];
|
||||
}
|
||||
}
|
||||
return curves;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal class Block
|
||||
{
|
||||
public Atrac9Config Config { get; }
|
||||
public BlockType BlockType { get; }
|
||||
public int BlockIndex { get; }
|
||||
public Frame Frame { get; }
|
||||
|
||||
public Channel[] Channels { get; }
|
||||
public int ChannelCount { get; }
|
||||
|
||||
public bool FirstInSuperframe { get; set; }
|
||||
public bool ReuseBandParams { get; set; }
|
||||
|
||||
public int BandCount { get; set; }
|
||||
public int StereoBand { get; set; }
|
||||
public int ExtensionBand { get; set; }
|
||||
public int QuantizationUnitCount { get; set; }
|
||||
public int StereoQuantizationUnit { get; set; }
|
||||
public int ExtensionUnit { get; set; }
|
||||
public int QuantizationUnitsPrev { get; set; }
|
||||
|
||||
public int[] Gradient { get; } = new int[31];
|
||||
public int GradientMode { get; set; }
|
||||
public int GradientStartUnit { get; set; }
|
||||
public int GradientStartValue { get; set; }
|
||||
public int GradientEndUnit { get; set; }
|
||||
public int GradientEndValue { get; set; }
|
||||
public int GradientBoundary { get; set; }
|
||||
|
||||
public int PrimaryChannelIndex { get; set; }
|
||||
public int[] JointStereoSigns { get; } = new int[30];
|
||||
public bool HasJointStereoSigns { get; set; }
|
||||
public Channel PrimaryChannel => Channels[PrimaryChannelIndex == 0 ? 0 : 1];
|
||||
public Channel SecondaryChannel => Channels[PrimaryChannelIndex == 0 ? 1 : 0];
|
||||
|
||||
public bool BandExtensionEnabled { get; set; }
|
||||
public bool HasExtensionData { get; set; }
|
||||
public int BexDataLength { get; set; }
|
||||
public int BexMode { get; set; }
|
||||
|
||||
public Block(Frame parentFrame, int blockIndex)
|
||||
{
|
||||
Frame = parentFrame;
|
||||
BlockIndex = blockIndex;
|
||||
Config = parentFrame.Config;
|
||||
BlockType = Config.ChannelConfig.BlockTypes[blockIndex];
|
||||
ChannelCount = BlockTypeToChannelCount(BlockType);
|
||||
Channels = new Channel[ChannelCount];
|
||||
for (int i = 0; i < ChannelCount; i++)
|
||||
{
|
||||
Channels[i] = new Channel(this, i);
|
||||
}
|
||||
}
|
||||
|
||||
public static int BlockTypeToChannelCount(BlockType blockType)
|
||||
{
|
||||
switch (blockType)
|
||||
{
|
||||
case BlockType.Mono:
|
||||
return 1;
|
||||
case BlockType.Stereo:
|
||||
return 2;
|
||||
case BlockType.LFE:
|
||||
return 1;
|
||||
default:
|
||||
return 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// An ATRAC9 block (substream) type
|
||||
/// </summary>
|
||||
public enum BlockType
|
||||
{
|
||||
/// <summary>
|
||||
/// Mono ATRAC9 block
|
||||
/// </summary>
|
||||
Mono = 0,
|
||||
/// <summary>
|
||||
/// Stereo ATRAC9 block
|
||||
/// </summary>
|
||||
Stereo = 1,
|
||||
/// <summary>
|
||||
/// Low-frequency effects ATRAC9 block
|
||||
/// </summary>
|
||||
LFE = 2
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
#nullable disable
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal class Channel
|
||||
{
|
||||
public Atrac9Config Config { get; }
|
||||
public int ChannelIndex { get; }
|
||||
public bool IsPrimary => Block.PrimaryChannelIndex == ChannelIndex;
|
||||
public Block Block { get; }
|
||||
public Mdct Mdct { get; }
|
||||
|
||||
public double[] Pcm { get; } = new double[256];
|
||||
public double[] Spectra { get; } = new double[256];
|
||||
|
||||
public int CodedQuantUnits { get; set; }
|
||||
public int ScaleFactorCodingMode { get; set; }
|
||||
public int[] ScaleFactors { get; } = new int[31];
|
||||
public int[] ScaleFactorsPrev { get; } = new int[31];
|
||||
|
||||
public int[] Precisions { get; } = new int[30];
|
||||
public int[] PrecisionsFine { get; } = new int[30];
|
||||
public int[] PrecisionMask { get; } = new int[30];
|
||||
|
||||
public int[] SpectraValuesBuffer { get; } = new int[16];
|
||||
public int[] CodebookSet { get; } = new int[30];
|
||||
|
||||
public int[] QuantizedSpectra { get; } = new int[256];
|
||||
public int[] QuantizedSpectraFine { get; } = new int[256];
|
||||
|
||||
public int BexMode { get; set; }
|
||||
public int BexValueCount { get; set; }
|
||||
public int[] BexValues { get; } = new int[4];
|
||||
public double[] BexScales { get; } = new double[6];
|
||||
public Atrac9Rng Rng { get; set; }
|
||||
|
||||
public Channel(Block parentBlock, int channelIndex)
|
||||
{
|
||||
Block = parentBlock;
|
||||
ChannelIndex = channelIndex;
|
||||
Config = parentBlock.Config;
|
||||
Mdct = new Mdct(Config.FrameSamplesPower, Tables.ImdctWindow[Config.FrameSamplesPower - 6]);
|
||||
}
|
||||
|
||||
public void UpdateCodedUnits() =>
|
||||
CodedQuantUnits = IsPrimary ? Block.QuantizationUnitCount : Block.StereoQuantizationUnit;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9
|
||||
{
|
||||
/// <summary>
|
||||
/// Describes the channel mapping for an ATRAC9 stream
|
||||
/// </summary>
|
||||
public class ChannelConfig
|
||||
{
|
||||
internal ChannelConfig(params BlockType[] blockTypes)
|
||||
{
|
||||
BlockCount = blockTypes.Length;
|
||||
BlockTypes = blockTypes;
|
||||
foreach (BlockType type in blockTypes)
|
||||
{
|
||||
ChannelCount += Block.BlockTypeToChannelCount(type);
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// The number of blocks or substreams in the ATRAC9 stream
|
||||
/// </summary>
|
||||
public int BlockCount { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The type of each block or substream in the ATRAC9 stream
|
||||
/// </summary>
|
||||
public BlockType[] BlockTypes { get; }
|
||||
|
||||
/// <summary>
|
||||
/// The number of channels in the ATRAC9 stream
|
||||
/// </summary>
|
||||
public int ChannelCount { get; }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal class Frame
|
||||
{
|
||||
public Atrac9Config Config { get; }
|
||||
public int FrameIndex { get; set; }
|
||||
public Block[] Blocks { get; }
|
||||
|
||||
public Frame(Atrac9Config config)
|
||||
{
|
||||
Config = config;
|
||||
Blocks = new Block[config.ChannelConfig.BlockCount];
|
||||
|
||||
for (int i = 0; i < config.ChannelConfig.BlockCount; i++)
|
||||
{
|
||||
Blocks[i] = new Block(this, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,63 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal class HuffmanCodebook
|
||||
{
|
||||
public HuffmanCodebook(short[] codes, byte[] bits, byte valueCountPower)
|
||||
{
|
||||
Codes = codes;
|
||||
Bits = bits;
|
||||
if (Codes == null || Bits == null) return;
|
||||
|
||||
ValueCount = 1 << valueCountPower;
|
||||
ValueCountPower = valueCountPower;
|
||||
ValueBits = Helpers.Log2(codes.Length) >> valueCountPower;
|
||||
ValueMax = 1 << ValueBits;
|
||||
|
||||
int max = 0;
|
||||
foreach (byte bitSize in bits)
|
||||
{
|
||||
max = Math.Max(max, bitSize);
|
||||
}
|
||||
|
||||
MaxBitSize = max;
|
||||
Lookup = CreateLookupTable();
|
||||
}
|
||||
|
||||
private byte[] CreateLookupTable()
|
||||
{
|
||||
if (Codes == null || Bits == null) return null;
|
||||
|
||||
int tableSize = 1 << MaxBitSize;
|
||||
var dest = new byte[tableSize];
|
||||
|
||||
for (int i = 0; i < Bits.Length; i++)
|
||||
{
|
||||
if (Bits[i] == 0) continue;
|
||||
int unusedBits = MaxBitSize - Bits[i];
|
||||
|
||||
int start = Codes[i] << unusedBits;
|
||||
int length = 1 << unusedBits;
|
||||
int end = start + length;
|
||||
|
||||
for (int j = start; j < end; j++)
|
||||
{
|
||||
dest[j] = (byte)i;
|
||||
}
|
||||
}
|
||||
return dest;
|
||||
}
|
||||
|
||||
public short[] Codes { get; }
|
||||
public byte[] Bits { get; }
|
||||
public byte[] Lookup { get; }
|
||||
public int ValueCount { get; }
|
||||
public int ValueCountPower { get; }
|
||||
public int ValueBits { get; }
|
||||
public int ValueMax { get; }
|
||||
public int MaxBitSize { get; }
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2018 Alex Barney
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,58 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class Quantization
|
||||
{
|
||||
public static void DequantizeSpectra(Block block)
|
||||
{
|
||||
foreach (Channel channel in block.Channels)
|
||||
{
|
||||
Array.Clear(channel.Spectra, 0, channel.Spectra.Length);
|
||||
|
||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
||||
{
|
||||
DequantizeQuantUnit(channel, i);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DequantizeQuantUnit(Channel channel, int band)
|
||||
{
|
||||
int subBandIndex = Tables.QuantUnitToCoeffIndex[band];
|
||||
int subBandCount = Tables.QuantUnitToCoeffCount[band];
|
||||
double stepSize = Tables.QuantizerStepSize[channel.Precisions[band]];
|
||||
double stepSizeFine = Tables.QuantizerFineStepSize[channel.PrecisionsFine[band]];
|
||||
|
||||
for (int sb = 0; sb < subBandCount; sb++)
|
||||
{
|
||||
double coarse = channel.QuantizedSpectra[subBandIndex + sb] * stepSize;
|
||||
double fine = channel.QuantizedSpectraFine[subBandIndex + sb] * stepSizeFine;
|
||||
channel.Spectra[subBandIndex + sb] = coarse + fine;
|
||||
}
|
||||
}
|
||||
|
||||
public static void ScaleSpectrum(Block block)
|
||||
{
|
||||
foreach (Channel channel in block.Channels)
|
||||
{
|
||||
ScaleSpectrum(channel);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ScaleSpectrum(Channel channel)
|
||||
{
|
||||
int quantUnitCount = channel.Block.QuantizationUnitCount;
|
||||
double[] spectra = channel.Spectra;
|
||||
|
||||
for (int i = 0; i < quantUnitCount; i++)
|
||||
{
|
||||
for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++)
|
||||
{
|
||||
spectra[sb] *= Tables.SpectrumScale[channel.ScaleFactors[i]];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,172 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.IO;
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class ScaleFactors
|
||||
{
|
||||
public static void Read(BitReader reader, Channel channel)
|
||||
{
|
||||
Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length);
|
||||
|
||||
channel.ScaleFactorCodingMode = reader.ReadInt(2);
|
||||
if (channel.ChannelIndex == 0)
|
||||
{
|
||||
switch (channel.ScaleFactorCodingMode)
|
||||
{
|
||||
case 0:
|
||||
ReadVlcDeltaOffset(reader, channel);
|
||||
break;
|
||||
case 1:
|
||||
ReadClcOffset(reader, channel);
|
||||
break;
|
||||
case 2:
|
||||
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
|
||||
ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
|
||||
break;
|
||||
case 3:
|
||||
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
|
||||
ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
switch (channel.ScaleFactorCodingMode)
|
||||
{
|
||||
case 0:
|
||||
ReadVlcDeltaOffset(reader, channel);
|
||||
break;
|
||||
case 1:
|
||||
ReadVlcDistanceToBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit);
|
||||
break;
|
||||
case 2:
|
||||
ReadVlcDeltaOffsetWithBaseline(reader, channel, channel.Block.Channels[0].ScaleFactors, channel.Block.ExtensionUnit);
|
||||
break;
|
||||
case 3:
|
||||
if (channel.Block.FirstInSuperframe) throw new InvalidDataException();
|
||||
ReadVlcDistanceToBaseline(reader, channel, channel.ScaleFactorsPrev, channel.Block.QuantizationUnitsPrev);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
if (channel.ScaleFactors[i] < 0 || channel.ScaleFactors[i] > 31)
|
||||
{
|
||||
throw new InvalidDataException("Scale factor values are out of range.");
|
||||
}
|
||||
}
|
||||
|
||||
Array.Copy(channel.ScaleFactors, channel.ScaleFactorsPrev, channel.ScaleFactors.Length);
|
||||
}
|
||||
|
||||
private static void ReadClcOffset(BitReader reader, Channel channel)
|
||||
{
|
||||
const int maxBits = 5;
|
||||
int[] sf = channel.ScaleFactors;
|
||||
int bitLength = reader.ReadInt(2) + 2;
|
||||
int baseValue = bitLength < maxBits ? reader.ReadInt(maxBits) : 0;
|
||||
|
||||
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
sf[i] = reader.ReadInt(bitLength) + baseValue;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadVlcDeltaOffset(BitReader reader, Channel channel)
|
||||
{
|
||||
int weightIndex = reader.ReadInt(3);
|
||||
byte[] weights = ScaleFactorWeights[weightIndex];
|
||||
|
||||
int[] sf = channel.ScaleFactors;
|
||||
int baseValue = reader.ReadInt(5);
|
||||
int bitLength = reader.ReadInt(2) + 3;
|
||||
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength];
|
||||
|
||||
sf[0] = reader.ReadInt(bitLength);
|
||||
|
||||
for (int i = 1; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
int delta = Unpack.ReadHuffmanValue(codebook, reader);
|
||||
sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
sf[i] += baseValue - weights[i];
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadVlcDistanceToBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength)
|
||||
{
|
||||
int[] sf = channel.ScaleFactors;
|
||||
int bitLength = reader.ReadInt(2) + 2;
|
||||
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsSigned[bitLength];
|
||||
int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength);
|
||||
|
||||
for (int i = 0; i < unitCount; i++)
|
||||
{
|
||||
int distance = Unpack.ReadHuffmanValue(codebook, reader, true);
|
||||
sf[i] = (baseline[i] + distance) & 31;
|
||||
}
|
||||
|
||||
for (int i = unitCount; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
sf[i] = reader.ReadInt(5);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadVlcDeltaOffsetWithBaseline(BitReader reader, Channel channel, int[] baseline, int baselineLength)
|
||||
{
|
||||
int[] sf = channel.ScaleFactors;
|
||||
int baseValue = reader.ReadOffsetBinary(5, BitReader.OffsetBias.Negative);
|
||||
int bitLength = reader.ReadInt(2) + 1;
|
||||
HuffmanCodebook codebook = Tables.HuffmanScaleFactorsUnsigned[bitLength];
|
||||
int unitCount = Math.Min(channel.Block.ExtensionUnit, baselineLength);
|
||||
|
||||
sf[0] = reader.ReadInt(bitLength);
|
||||
|
||||
for (int i = 1; i < unitCount; i++)
|
||||
{
|
||||
int delta = Unpack.ReadHuffmanValue(codebook, reader);
|
||||
sf[i] = (sf[i - 1] + delta) & (codebook.ValueMax - 1);
|
||||
}
|
||||
|
||||
for (int i = 0; i < unitCount; i++)
|
||||
{
|
||||
sf[i] += baseValue + baseline[i];
|
||||
}
|
||||
|
||||
for (int i = unitCount; i < channel.Block.ExtensionUnit; i++)
|
||||
{
|
||||
sf[i] = reader.ReadInt(5);
|
||||
}
|
||||
}
|
||||
|
||||
public static readonly byte[][] ScaleFactorWeights =
|
||||
{
|
||||
new byte[] {
|
||||
0, 0, 0, 1, 1, 2, 2, 2, 2, 2, 2, 3, 2, 3, 3, 4, 4, 4, 4, 4, 4, 5, 5, 6, 6, 7, 7, 8, 10, 12, 12, 12
|
||||
}, new byte[] {
|
||||
3, 2, 2, 1, 1, 1, 1, 1, 0, 1, 1, 1, 0, 0, 0, 1, 0, 1, 1, 1, 1, 1, 1, 2, 3, 3, 4, 5, 7, 10, 10, 10
|
||||
}, new byte[] {
|
||||
0, 2, 4, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 6, 7, 7, 7, 7, 8, 9, 12, 12, 12
|
||||
}, new byte[] {
|
||||
0, 1, 1, 2, 2, 2, 3, 3, 3, 3, 3, 4, 4, 4, 5, 5, 5, 6, 6, 6, 6, 7, 8, 8, 10, 11, 11, 12, 13, 13, 13, 13
|
||||
}, new byte[] {
|
||||
0, 2, 2, 3, 3, 4, 4, 5, 4, 5, 5, 5, 5, 6, 7, 8, 8, 8, 8, 9, 9, 9, 10, 10, 11, 12, 12, 13, 13, 14, 14, 14
|
||||
}, new byte[] {
|
||||
1, 1, 0, 0, 0, 0, 1, 0, 0, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 3, 3, 3, 4, 4, 5, 6, 7, 7, 9, 11, 11, 11
|
||||
}, new byte[] {
|
||||
0, 5, 8, 10, 11, 11, 12, 12, 12, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 12, 12, 12,
|
||||
12, 13, 15, 15, 15
|
||||
}, new byte[] {
|
||||
0, 2, 3, 4, 5, 6, 6, 7, 7, 8, 8, 8, 9, 9, 10, 10, 10, 11, 11, 11, 11, 11, 11, 12, 12, 12, 12, 13, 13,
|
||||
15, 15, 15
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class Stereo
|
||||
{
|
||||
public static void ApplyIntensityStereo(Block block)
|
||||
{
|
||||
if (block.BlockType != BlockType.Stereo) return;
|
||||
|
||||
int totalUnits = block.QuantizationUnitCount;
|
||||
int stereoUnits = block.StereoQuantizationUnit;
|
||||
if (stereoUnits >= totalUnits) return;
|
||||
|
||||
Channel source = block.PrimaryChannel;
|
||||
Channel dest = block.SecondaryChannel;
|
||||
|
||||
for (int i = stereoUnits; i < totalUnits; i++)
|
||||
{
|
||||
int sign = block.JointStereoSigns[i];
|
||||
for (int sb = Tables.QuantUnitToCoeffIndex[i]; sb < Tables.QuantUnitToCoeffIndex[i + 1]; sb++)
|
||||
{
|
||||
if (sign > 0)
|
||||
{
|
||||
dest.Spectra[sb] = -source.Spectra[sb];
|
||||
}
|
||||
else
|
||||
{
|
||||
dest.Spectra[sb] = source.Spectra[sb];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,116 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using static LibAtrac9.HuffmanCodebooks;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class Tables
|
||||
{
|
||||
public static int MaxHuffPrecision(bool highSampleRate) => highSampleRate ? 1 : 7;
|
||||
public static int MinBandCount(bool highSampleRate) => highSampleRate ? 1 : 3;
|
||||
public static int MaxExtensionBand(bool highSampleRate) => highSampleRate ? 16 : 18;
|
||||
|
||||
public static readonly int[] SampleRates =
|
||||
{
|
||||
11025, 12000, 16000, 22050, 24000, 32000, 44100, 48000,
|
||||
44100, 48000, 64000, 88200, 96000, 128000, 176400, 192000
|
||||
};
|
||||
|
||||
public static readonly byte[] SamplingRateIndexToFrameSamplesPower = { 6, 6, 7, 7, 7, 8, 8, 8, 6, 6, 7, 7, 7, 8, 8, 8 };
|
||||
|
||||
// From sampling rate index
|
||||
public static readonly byte[] MaxBandCount = { 8, 8, 12, 12, 12, 18, 18, 18, 8, 8, 12, 12, 12, 16, 16, 16 };
|
||||
public static readonly byte[] BandToQuantUnitCount = { 0, 4, 8, 10, 12, 13, 14, 15, 16, 18, 20, 21, 22, 23, 24, 25, 26, 28, 30 };
|
||||
|
||||
public static readonly byte[] QuantUnitToCoeffCount =
|
||||
{
|
||||
02, 02, 02, 02, 02, 02, 02, 02, 04, 04, 04, 04, 08, 08, 08,
|
||||
08, 08, 08, 08, 08, 16, 16, 16, 16, 16, 16, 16, 16, 16, 16
|
||||
};
|
||||
|
||||
public static readonly short[] QuantUnitToCoeffIndex =
|
||||
{
|
||||
0, 2, 4, 6, 8, 10, 12, 14, 16, 20, 24, 28, 32, 40, 48, 56,
|
||||
64, 72, 80, 88, 96, 112, 128, 144, 160, 176, 192, 208, 224, 240, 256
|
||||
};
|
||||
|
||||
public static readonly byte[] QuantUnitToCodebookIndex =
|
||||
{
|
||||
0, 0, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 2, 2, 2,
|
||||
2, 2, 2, 2, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3
|
||||
};
|
||||
|
||||
public static readonly ChannelConfig[] ChannelConfig =
|
||||
{
|
||||
new ChannelConfig(BlockType.Mono),
|
||||
new ChannelConfig(BlockType.Mono, BlockType.Mono),
|
||||
new ChannelConfig(BlockType.Stereo),
|
||||
new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo),
|
||||
new ChannelConfig(BlockType.Stereo, BlockType.Mono, BlockType.LFE, BlockType.Stereo, BlockType.Stereo),
|
||||
new ChannelConfig(BlockType.Stereo, BlockType.Stereo)
|
||||
};
|
||||
|
||||
public static readonly HuffmanCodebook[] HuffmanScaleFactorsUnsigned =
|
||||
GenerateHuffmanCodebooks(HuffmanScaleFactorsACodes, HuffmanScaleFactorsABits, HuffmanScaleFactorsGroupSizes);
|
||||
|
||||
public static readonly HuffmanCodebook[] HuffmanScaleFactorsSigned =
|
||||
GenerateHuffmanCodebooks(HuffmanScaleFactorsBCodes, HuffmanScaleFactorsBBits, HuffmanScaleFactorsGroupSizes);
|
||||
|
||||
public static readonly HuffmanCodebook[][][] HuffmanSpectrum =
|
||||
{
|
||||
GenerateHuffmanCodebooks(HuffmanSpectrumACodes, HuffmanSpectrumABits, HuffmanSpectrumAGroupSizes),
|
||||
GenerateHuffmanCodebooks(HuffmanSpectrumBCodes, HuffmanSpectrumBBits, HuffmanSpectrumBGroupSizes)
|
||||
};
|
||||
|
||||
public static readonly double[][] ImdctWindow = { GenerateImdctWindow(6), GenerateImdctWindow(7), GenerateImdctWindow(8) };
|
||||
|
||||
public static readonly double[] SpectrumScale = Generate(32, SpectrumScaleFunction);
|
||||
public static readonly double[] QuantizerStepSize = Generate(16, QuantizerStepSizeFunction);
|
||||
public static readonly double[] QuantizerFineStepSize = Generate(16, QuantizerFineStepSizeFunction);
|
||||
|
||||
public static readonly byte[][] GradientCurves = BitAllocation.GenerateGradientCurves();
|
||||
|
||||
private static double QuantizerStepSizeFunction(int x) => 2.0 / ((1 << (x + 1)) - 1);
|
||||
private static double QuantizerFineStepSizeFunction(int x) => QuantizerStepSizeFunction(x) / ushort.MaxValue;
|
||||
private static double SpectrumScaleFunction(int x) => Math.Pow(2, x - 15);
|
||||
|
||||
private static double[] GenerateImdctWindow(int frameSizePower)
|
||||
{
|
||||
int frameSize = 1 << frameSizePower;
|
||||
var output = new double[frameSize];
|
||||
|
||||
double[] a1 = GenerateMdctWindow(frameSizePower);
|
||||
|
||||
for (int i = 0; i < frameSize; i++)
|
||||
{
|
||||
output[i] = a1[i] / (a1[frameSize - 1 - i] * a1[frameSize - 1 - i] + a1[i] * a1[i]);
|
||||
}
|
||||
return output;
|
||||
}
|
||||
|
||||
private static double[] GenerateMdctWindow(int frameSizePower)
|
||||
{
|
||||
int frameSize = 1 << frameSizePower;
|
||||
var output = new double[frameSize];
|
||||
|
||||
for (int i = 0; i < frameSize; i++)
|
||||
{
|
||||
output[i] = (Math.Sin(((i + 0.5) / frameSize - 0.5) * Math.PI) + 1.0) * 0.5;
|
||||
}
|
||||
|
||||
return output;
|
||||
}
|
||||
|
||||
private static T[] Generate<T>(int count, Func<int, T> elementGenerator)
|
||||
{
|
||||
var table = new T[count];
|
||||
for (int i = 0; i < count; i++)
|
||||
{
|
||||
table[i] = elementGenerator(i);
|
||||
}
|
||||
return table;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,426 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.IO;
|
||||
using LibAtrac9.Utilities;
|
||||
|
||||
namespace LibAtrac9
|
||||
{
|
||||
internal static class Unpack
|
||||
{
|
||||
public static void UnpackFrame(BitReader reader, Frame frame)
|
||||
{
|
||||
foreach (Block block in frame.Blocks)
|
||||
{
|
||||
UnpackBlock(reader, block);
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnpackBlock(BitReader reader, Block block)
|
||||
{
|
||||
ReadBlockHeader(reader, block);
|
||||
|
||||
if (block.BlockType == BlockType.LFE)
|
||||
{
|
||||
UnpackLfeBlock(reader, block);
|
||||
}
|
||||
else
|
||||
{
|
||||
UnpackStandardBlock(reader, block);
|
||||
}
|
||||
|
||||
reader.AlignPosition(8);
|
||||
}
|
||||
|
||||
private static void ReadBlockHeader(BitReader reader, Block block)
|
||||
{
|
||||
bool firstInSuperframe = block.Frame.FrameIndex == 0;
|
||||
block.FirstInSuperframe = !reader.ReadBool();
|
||||
block.ReuseBandParams = reader.ReadBool();
|
||||
|
||||
if (block.FirstInSuperframe != firstInSuperframe)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
|
||||
if (firstInSuperframe && block.ReuseBandParams && block.BlockType != BlockType.LFE)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void UnpackStandardBlock(BitReader reader, Block block)
|
||||
{
|
||||
Channel[] channels = block.Channels;
|
||||
|
||||
if (!block.ReuseBandParams)
|
||||
{
|
||||
ReadBandParams(reader, block);
|
||||
}
|
||||
|
||||
ReadGradientParams(reader, block);
|
||||
BitAllocation.CreateGradient(block);
|
||||
ReadStereoParams(reader, block);
|
||||
ReadExtensionParams(reader, block);
|
||||
|
||||
foreach (Channel channel in channels)
|
||||
{
|
||||
channel.UpdateCodedUnits();
|
||||
|
||||
ScaleFactors.Read(reader, channel);
|
||||
BitAllocation.CalculateMask(channel);
|
||||
BitAllocation.CalculatePrecisions(channel);
|
||||
CalculateSpectrumCodebookIndex(channel);
|
||||
|
||||
ReadSpectra(reader, channel);
|
||||
ReadSpectraFine(reader, channel);
|
||||
}
|
||||
|
||||
block.QuantizationUnitsPrev = block.BandExtensionEnabled ? block.ExtensionUnit : block.QuantizationUnitCount;
|
||||
}
|
||||
|
||||
private static void ReadBandParams(BitReader reader, Block block)
|
||||
{
|
||||
int minBandCount = Tables.MinBandCount(block.Config.HighSampleRate);
|
||||
int maxExtensionBand = Tables.MaxExtensionBand(block.Config.HighSampleRate);
|
||||
block.BandCount = reader.ReadInt(4);
|
||||
block.BandCount += minBandCount;
|
||||
block.QuantizationUnitCount = Tables.BandToQuantUnitCount[block.BandCount];
|
||||
if (block.BandCount < minBandCount || block.BandCount >
|
||||
Tables.MaxBandCount[block.Config.SampleRateIndex])
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
if (block.BlockType == BlockType.Stereo)
|
||||
{
|
||||
block.StereoBand = reader.ReadInt(4);
|
||||
block.StereoBand += minBandCount;
|
||||
block.StereoQuantizationUnit = Tables.BandToQuantUnitCount[block.StereoBand];
|
||||
}
|
||||
else
|
||||
{
|
||||
block.StereoBand = block.BandCount;
|
||||
}
|
||||
|
||||
block.BandExtensionEnabled = reader.ReadBool();
|
||||
if (block.BandExtensionEnabled)
|
||||
{
|
||||
block.ExtensionBand = reader.ReadInt(4);
|
||||
block.ExtensionBand += minBandCount;
|
||||
|
||||
if (block.ExtensionBand < block.BandCount || block.ExtensionBand > maxExtensionBand)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
|
||||
block.ExtensionUnit = Tables.BandToQuantUnitCount[block.ExtensionBand];
|
||||
}
|
||||
else
|
||||
{
|
||||
block.ExtensionBand = block.BandCount;
|
||||
block.ExtensionUnit = block.QuantizationUnitCount;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadGradientParams(BitReader reader, Block block)
|
||||
{
|
||||
block.GradientMode = reader.ReadInt(2);
|
||||
if (block.GradientMode > 0)
|
||||
{
|
||||
block.GradientEndUnit = 31;
|
||||
block.GradientEndValue = 31;
|
||||
block.GradientStartUnit = reader.ReadInt(5);
|
||||
block.GradientStartValue = reader.ReadInt(5);
|
||||
}
|
||||
else
|
||||
{
|
||||
block.GradientStartUnit = reader.ReadInt(6);
|
||||
block.GradientEndUnit = reader.ReadInt(6) + 1;
|
||||
block.GradientStartValue = reader.ReadInt(5);
|
||||
block.GradientEndValue = reader.ReadInt(5);
|
||||
}
|
||||
block.GradientBoundary = reader.ReadInt(4);
|
||||
|
||||
if (block.GradientBoundary > block.QuantizationUnitCount)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
if (block.GradientStartUnit < 1 || block.GradientStartUnit >= 48)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
if (block.GradientEndUnit < 1 || block.GradientEndUnit >= 48)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
if (block.GradientStartUnit > block.GradientEndUnit)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
if (block.GradientStartValue < 0 || block.GradientStartValue >= 32)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
if (block.GradientEndValue < 0 || block.GradientEndValue >= 32)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadStereoParams(BitReader reader, Block block)
|
||||
{
|
||||
if (block.BlockType != BlockType.Stereo) return;
|
||||
|
||||
block.PrimaryChannelIndex = reader.ReadInt(1);
|
||||
block.HasJointStereoSigns = reader.ReadBool();
|
||||
if (block.HasJointStereoSigns)
|
||||
{
|
||||
for (int i = block.StereoQuantizationUnit; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
block.JointStereoSigns[i] = reader.ReadInt(1);
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Array.Clear(block.JointStereoSigns, 0, block.JointStereoSigns.Length);
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadExtensionParams(BitReader reader, Block block)
|
||||
{
|
||||
// ReSharper disable once RedundantAssignment
|
||||
int bexBand = 0;
|
||||
if (block.BandExtensionEnabled)
|
||||
{
|
||||
BandExtension.GetBexBandInfo(out bexBand, out _, out _, block.QuantizationUnitCount);
|
||||
if (block.BlockType == BlockType.Stereo)
|
||||
{
|
||||
ReadHeader(block.Channels[1]);
|
||||
}
|
||||
else
|
||||
{
|
||||
reader.Position += 1;
|
||||
}
|
||||
}
|
||||
block.HasExtensionData = reader.ReadBool();
|
||||
|
||||
if (!block.HasExtensionData) return;
|
||||
if (!block.BandExtensionEnabled)
|
||||
{
|
||||
block.BexMode = reader.ReadInt(2);
|
||||
block.BexDataLength = reader.ReadInt(5);
|
||||
reader.Position += block.BexDataLength;
|
||||
return;
|
||||
}
|
||||
|
||||
ReadHeader(block.Channels[0]);
|
||||
|
||||
block.BexDataLength = reader.ReadInt(5);
|
||||
if (block.BexDataLength <= 0) return;
|
||||
int bexDataEnd = reader.Position + block.BexDataLength;
|
||||
|
||||
ReadData(block.Channels[0]);
|
||||
|
||||
if (block.BlockType == BlockType.Stereo)
|
||||
{
|
||||
ReadData(block.Channels[1]);
|
||||
}
|
||||
|
||||
// Make sure we didn't read too many bits
|
||||
if (reader.Position > bexDataEnd)
|
||||
{
|
||||
throw new InvalidDataException();
|
||||
}
|
||||
|
||||
void ReadHeader(Channel channel)
|
||||
{
|
||||
int bexMode = reader.ReadInt(2);
|
||||
channel.BexMode = bexBand > 2 ? bexMode : 4;
|
||||
channel.BexValueCount = BandExtension.BexEncodedValueCounts[channel.BexMode][bexBand];
|
||||
}
|
||||
|
||||
void ReadData(Channel channel)
|
||||
{
|
||||
for (int i = 0; i < channel.BexValueCount; i++)
|
||||
{
|
||||
int dataLength = BandExtension.BexDataLengths[channel.BexMode][bexBand][i];
|
||||
channel.BexValues[i] = reader.ReadInt(dataLength);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void CalculateSpectrumCodebookIndex(Channel channel)
|
||||
{
|
||||
Array.Clear(channel.CodebookSet, 0, channel.CodebookSet.Length);
|
||||
int quantUnits = channel.CodedQuantUnits;
|
||||
int[] sf = channel.ScaleFactors;
|
||||
|
||||
if (quantUnits <= 1) return;
|
||||
if (channel.Config.HighSampleRate) return;
|
||||
|
||||
// Temporarily setting this value allows for simpler code by
|
||||
// making the last value a non-special case.
|
||||
int originalScaleTmp = sf[quantUnits];
|
||||
sf[quantUnits] = sf[quantUnits - 1];
|
||||
|
||||
int avg = 0;
|
||||
if (quantUnits > 12)
|
||||
{
|
||||
for (int i = 0; i < 12; i++)
|
||||
{
|
||||
avg += sf[i];
|
||||
}
|
||||
avg = (avg + 6) / 12;
|
||||
}
|
||||
|
||||
for (int i = 8; i < quantUnits; i++)
|
||||
{
|
||||
int prevSf = sf[i - 1];
|
||||
int nextSf = sf[i + 1];
|
||||
int minSf = Math.Min(prevSf, nextSf);
|
||||
if (sf[i] - minSf >= 3 || sf[i] - prevSf + sf[i] - nextSf >= 3)
|
||||
{
|
||||
channel.CodebookSet[i] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 12; i < quantUnits; i++)
|
||||
{
|
||||
if (channel.CodebookSet[i] == 0)
|
||||
{
|
||||
int minSf = Math.Min(sf[i - 1], sf[i + 1]);
|
||||
if (sf[i] - minSf >= 2 && sf[i] >= avg - (Tables.QuantUnitToCoeffCount[i] == 16 ? 1 : 0))
|
||||
{
|
||||
channel.CodebookSet[i] = 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sf[quantUnits] = originalScaleTmp;
|
||||
}
|
||||
|
||||
private static void ReadSpectra(BitReader reader, Channel channel)
|
||||
{
|
||||
int[] values = channel.SpectraValuesBuffer;
|
||||
Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length);
|
||||
int maxHuffPrecision = Tables.MaxHuffPrecision(channel.Config.HighSampleRate);
|
||||
|
||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
||||
{
|
||||
int subbandCount = Tables.QuantUnitToCoeffCount[i];
|
||||
int precision = channel.Precisions[i] + 1;
|
||||
if (precision <= maxHuffPrecision)
|
||||
{
|
||||
HuffmanCodebook huff = Tables.HuffmanSpectrum[channel.CodebookSet[i]][precision][Tables.QuantUnitToCodebookIndex[i]];
|
||||
int groupCount = subbandCount >> huff.ValueCountPower;
|
||||
for (int j = 0; j < groupCount; j++)
|
||||
{
|
||||
values[j] = ReadHuffmanValue(huff, reader);
|
||||
}
|
||||
|
||||
DecodeHuffmanValues(channel.QuantizedSpectra, Tables.QuantUnitToCoeffIndex[i], subbandCount, huff, values);
|
||||
}
|
||||
else
|
||||
{
|
||||
int subbandIndex = Tables.QuantUnitToCoeffIndex[i];
|
||||
for (int j = subbandIndex; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++)
|
||||
{
|
||||
channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadSpectraFine(BitReader reader, Channel channel)
|
||||
{
|
||||
Array.Clear(channel.QuantizedSpectraFine, 0, channel.QuantizedSpectraFine.Length);
|
||||
|
||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
||||
{
|
||||
if (channel.PrecisionsFine[i] > 0)
|
||||
{
|
||||
int overflowBits = channel.PrecisionsFine[i] + 1;
|
||||
int startSubband = Tables.QuantUnitToCoeffIndex[i];
|
||||
int endSubband = Tables.QuantUnitToCoeffIndex[i + 1];
|
||||
|
||||
for (int j = startSubband; j < endSubband; j++)
|
||||
{
|
||||
channel.QuantizedSpectraFine[j] = reader.ReadSignedInt(overflowBits);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static void DecodeHuffmanValues(int[] spectrum, int index, int bandCount, HuffmanCodebook huff, int[] values)
|
||||
{
|
||||
int valueCount = bandCount >> huff.ValueCountPower;
|
||||
int mask = (1 << huff.ValueBits) - 1;
|
||||
|
||||
for (int i = 0; i < valueCount; i++)
|
||||
{
|
||||
int value = values[i];
|
||||
for (int j = 0; j < huff.ValueCount; j++)
|
||||
{
|
||||
spectrum[index++] = Bit.SignExtend32(value & mask, huff.ValueBits);
|
||||
value >>= huff.ValueBits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static int ReadHuffmanValue(HuffmanCodebook huff, BitReader reader, bool signed = false)
|
||||
{
|
||||
int code = reader.PeekInt(huff.MaxBitSize);
|
||||
byte value = huff.Lookup[code];
|
||||
int bits = huff.Bits[value];
|
||||
reader.Position += bits;
|
||||
return signed ? Bit.SignExtend32(value, huff.ValueBits) : value;
|
||||
}
|
||||
|
||||
private static void UnpackLfeBlock(BitReader reader, Block block)
|
||||
{
|
||||
Channel channel = block.Channels[0];
|
||||
block.QuantizationUnitCount = 2;
|
||||
|
||||
DecodeLfeScaleFactors(reader, channel);
|
||||
CalculateLfePrecision(channel);
|
||||
channel.CodedQuantUnits = block.QuantizationUnitCount;
|
||||
ReadLfeSpectra(reader, channel);
|
||||
}
|
||||
|
||||
private static void DecodeLfeScaleFactors(BitReader reader, Channel channel)
|
||||
{
|
||||
Array.Clear(channel.ScaleFactors, 0, channel.ScaleFactors.Length);
|
||||
for (int i = 0; i < channel.Block.QuantizationUnitCount; i++)
|
||||
{
|
||||
channel.ScaleFactors[i] = reader.ReadInt(5);
|
||||
}
|
||||
}
|
||||
|
||||
private static void CalculateLfePrecision(Channel channel)
|
||||
{
|
||||
Block block = channel.Block;
|
||||
int precision = block.ReuseBandParams ? 8 : 4;
|
||||
for (int i = 0; i < block.QuantizationUnitCount; i++)
|
||||
{
|
||||
channel.Precisions[i] = precision;
|
||||
channel.PrecisionsFine[i] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
private static void ReadLfeSpectra(BitReader reader, Channel channel)
|
||||
{
|
||||
Array.Clear(channel.QuantizedSpectra, 0, channel.QuantizedSpectra.Length);
|
||||
|
||||
for (int i = 0; i < channel.CodedQuantUnits; i++)
|
||||
{
|
||||
if (channel.Precisions[i] <= 0) continue;
|
||||
|
||||
int precision = channel.Precisions[i] + 1;
|
||||
for (int j = Tables.QuantUnitToCoeffIndex[i]; j < Tables.QuantUnitToCoeffIndex[i + 1]; j++)
|
||||
{
|
||||
channel.QuantizedSpectra[j] = reader.ReadSignedInt(precision);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
#nullable disable
|
||||
namespace LibAtrac9.Utilities
|
||||
{
|
||||
internal static class Bit
|
||||
{
|
||||
private static uint BitReverse32(uint value)
|
||||
{
|
||||
value = ((value & 0xaaaaaaaa) >> 1) | ((value & 0x55555555) << 1);
|
||||
value = ((value & 0xcccccccc) >> 2) | ((value & 0x33333333) << 2);
|
||||
value = ((value & 0xf0f0f0f0) >> 4) | ((value & 0x0f0f0f0f) << 4);
|
||||
value = ((value & 0xff00ff00) >> 8) | ((value & 0x00ff00ff) << 8);
|
||||
return (value >> 16) | (value << 16);
|
||||
}
|
||||
private static uint BitReverse32(uint value, int bitCount) => BitReverse32(value) >> (32 - bitCount);
|
||||
public static int BitReverse32(int value, int bitCount) => (int) BitReverse32((uint) value, bitCount);
|
||||
|
||||
public static int SignExtend32(int value, int bits)
|
||||
{
|
||||
int shift = 8 * sizeof(int) - bits;
|
||||
return (value << shift) >> shift;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,133 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Diagnostics;
|
||||
|
||||
namespace LibAtrac9.Utilities
|
||||
{
|
||||
internal class BitReader
|
||||
{
|
||||
private byte[] Buffer { get; set; }
|
||||
private int LengthBits { get; set; }
|
||||
public int Position { get; set; }
|
||||
private int Remaining => LengthBits - Position;
|
||||
|
||||
public BitReader(byte[] buffer) => SetBuffer(buffer);
|
||||
|
||||
public void SetBuffer(byte[] buffer)
|
||||
{
|
||||
Buffer = buffer;
|
||||
LengthBits = Buffer?.Length * 8 ?? 0;
|
||||
Position = 0;
|
||||
}
|
||||
|
||||
public int ReadInt(int bitCount)
|
||||
{
|
||||
int value = PeekInt(bitCount);
|
||||
Position += bitCount;
|
||||
return value;
|
||||
}
|
||||
|
||||
public int ReadSignedInt(int bitCount)
|
||||
{
|
||||
int value = PeekInt(bitCount);
|
||||
Position += bitCount;
|
||||
return Bit.SignExtend32(value, bitCount);
|
||||
}
|
||||
|
||||
public bool ReadBool() => ReadInt(1) == 1;
|
||||
|
||||
public int ReadOffsetBinary(int bitCount, OffsetBias bias)
|
||||
{
|
||||
int offset = (1 << (bitCount - 1)) - (int)bias;
|
||||
int value = PeekInt(bitCount) - offset;
|
||||
Position += bitCount;
|
||||
return value;
|
||||
}
|
||||
|
||||
public void AlignPosition(int multiple)
|
||||
{
|
||||
Position = Helpers.GetNextMultiple(Position, multiple);
|
||||
}
|
||||
|
||||
public int PeekInt(int bitCount)
|
||||
{
|
||||
Debug.Assert(bitCount >= 0 && bitCount <= 32);
|
||||
|
||||
if (bitCount > Remaining)
|
||||
{
|
||||
if (Position >= LengthBits) return 0;
|
||||
|
||||
int extraBits = bitCount - Remaining;
|
||||
return PeekIntFallback(Remaining) << extraBits;
|
||||
}
|
||||
|
||||
int byteIndex = Position / 8;
|
||||
int bitIndex = Position % 8;
|
||||
|
||||
if (bitCount <= 9 && Remaining >= 16)
|
||||
{
|
||||
int value = Buffer[byteIndex] << 8 | Buffer[byteIndex + 1];
|
||||
value &= 0xFFFF >> bitIndex;
|
||||
value >>= 16 - bitCount - bitIndex;
|
||||
return value;
|
||||
}
|
||||
|
||||
if (bitCount <= 17 && Remaining >= 24)
|
||||
{
|
||||
int value = Buffer[byteIndex] << 16 | Buffer[byteIndex + 1] << 8 | Buffer[byteIndex + 2];
|
||||
value &= 0xFFFFFF >> bitIndex;
|
||||
value >>= 24 - bitCount - bitIndex;
|
||||
return value;
|
||||
}
|
||||
|
||||
if (bitCount <= 25 && Remaining >= 32)
|
||||
{
|
||||
int value = Buffer[byteIndex] << 24 | Buffer[byteIndex + 1] << 16 | Buffer[byteIndex + 2] << 8 | Buffer[byteIndex + 3];
|
||||
value &= (int)(0xFFFFFFFF >> bitIndex);
|
||||
value >>= 32 - bitCount - bitIndex;
|
||||
return value;
|
||||
}
|
||||
return PeekIntFallback(bitCount);
|
||||
}
|
||||
|
||||
private int PeekIntFallback(int bitCount)
|
||||
{
|
||||
int value = 0;
|
||||
int byteIndex = Position / 8;
|
||||
int bitIndex = Position % 8;
|
||||
|
||||
while (bitCount > 0)
|
||||
{
|
||||
if (bitIndex >= 8)
|
||||
{
|
||||
bitIndex = 0;
|
||||
byteIndex++;
|
||||
}
|
||||
|
||||
int bitsToRead = Math.Min(bitCount, 8 - bitIndex);
|
||||
int mask = 0xFF >> bitIndex;
|
||||
int currentByte = (mask & Buffer[byteIndex]) >> (8 - bitIndex - bitsToRead);
|
||||
|
||||
value = (value << bitsToRead) | currentByte;
|
||||
bitIndex += bitsToRead;
|
||||
bitCount -= bitsToRead;
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Specifies the bias of an offset binary value. A positive bias can represent one more
|
||||
/// positive value than negative value, and a negative bias can represent one more
|
||||
/// negative value than positive value.
|
||||
/// </summary>
|
||||
/// <remarks>Example:
|
||||
/// A 4-bit offset binary value with a positive bias can store
|
||||
/// the values 8 through -7 inclusive.
|
||||
/// A 4-bit offset binary value with a positive bias can store
|
||||
/// the values 7 through -8 inclusive.</remarks>
|
||||
public enum OffsetBias
|
||||
{
|
||||
Negative = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,51 @@
|
||||
#nullable disable
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
namespace LibAtrac9.Utilities
|
||||
{
|
||||
internal static class Helpers
|
||||
{
|
||||
[MethodImpl(MethodImplOptions.AggressiveInlining)]
|
||||
public static short Clamp16(int value)
|
||||
{
|
||||
if (value > short.MaxValue)
|
||||
return short.MaxValue;
|
||||
if (value < short.MinValue)
|
||||
return short.MinValue;
|
||||
return (short)value;
|
||||
}
|
||||
|
||||
public static int GetNextMultiple(int value, int multiple)
|
||||
{
|
||||
if (multiple <= 0)
|
||||
return value;
|
||||
|
||||
if (value % multiple == 0)
|
||||
return value;
|
||||
|
||||
return value + multiple - value % multiple;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Returns the floor of the base 2 logarithm of a specified number.
|
||||
/// </summary>
|
||||
/// <param name="value">The number whose logarithm is to be found.</param>
|
||||
/// <returns>The floor of the base 2 logarithm of <paramref name="value"/>.</returns>
|
||||
public static int Log2(int value)
|
||||
{
|
||||
value |= value >> 1;
|
||||
value |= value >> 2;
|
||||
value |= value >> 4;
|
||||
value |= value >> 8;
|
||||
value |= value >> 16;
|
||||
|
||||
return MultiplyDeBruijnBitPosition[(uint)(value * 0x07C4ACDDU) >> 27];
|
||||
}
|
||||
|
||||
private static readonly int[] MultiplyDeBruijnBitPosition =
|
||||
{
|
||||
0, 9, 1, 10, 13, 21, 2, 29, 11, 14, 16, 18, 22, 25, 3, 30,
|
||||
8, 12, 20, 28, 15, 17, 24, 7, 19, 27, 23, 6, 26, 5, 4, 31
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,178 @@
|
||||
#nullable disable
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
|
||||
namespace LibAtrac9.Utilities
|
||||
{
|
||||
internal class Mdct
|
||||
{
|
||||
private int MdctBits { get; }
|
||||
private int MdctSize { get; }
|
||||
private double Scale { get; }
|
||||
|
||||
private static readonly object TableLock = new object();
|
||||
private static int _tableBits = -1;
|
||||
private static readonly List<double[]> SinTables = new List<double[]>();
|
||||
private static readonly List<double[]> CosTables = new List<double[]>();
|
||||
private static readonly List<int[]> ShuffleTables = new List<int[]>();
|
||||
|
||||
private readonly double[] _imdctPrevious;
|
||||
private readonly double[] _imdctWindow;
|
||||
|
||||
private readonly double[] _scratchMdct;
|
||||
private readonly double[] _scratchDct;
|
||||
|
||||
public Mdct(int mdctBits, double[] window, double scale = 1)
|
||||
{
|
||||
SetTables(mdctBits);
|
||||
|
||||
MdctBits = mdctBits;
|
||||
MdctSize = 1 << mdctBits;
|
||||
Scale = scale;
|
||||
|
||||
if (window.Length < MdctSize)
|
||||
{
|
||||
throw new ArgumentException("Window must be as long as the MDCT size.", nameof(window));
|
||||
}
|
||||
|
||||
_imdctPrevious = new double[MdctSize];
|
||||
_scratchMdct = new double[MdctSize];
|
||||
_scratchDct = new double[MdctSize];
|
||||
_imdctWindow = window;
|
||||
}
|
||||
|
||||
private static void SetTables(int maxBits)
|
||||
{
|
||||
lock (TableLock)
|
||||
{
|
||||
if (maxBits > _tableBits)
|
||||
{
|
||||
for (int i = _tableBits + 1; i <= maxBits; i++)
|
||||
{
|
||||
GenerateTrigTables(i, out double[] sin, out double[] cos);
|
||||
SinTables.Add(sin);
|
||||
CosTables.Add(cos);
|
||||
ShuffleTables.Add(GenerateShuffleTable(i));
|
||||
}
|
||||
_tableBits = maxBits;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void RunImdct(double[] input, double[] output)
|
||||
{
|
||||
if (input.Length < MdctSize)
|
||||
{
|
||||
throw new ArgumentException("Input must be as long as the MDCT size.", nameof(input));
|
||||
}
|
||||
|
||||
if (output.Length < MdctSize)
|
||||
{
|
||||
throw new ArgumentException("Output must be as long as the MDCT size.", nameof(output));
|
||||
}
|
||||
|
||||
int size = MdctSize;
|
||||
int half = size / 2;
|
||||
double[] dctOut = _scratchMdct;
|
||||
|
||||
Dct4(input, dctOut);
|
||||
|
||||
for (int i = 0; i < half; i++)
|
||||
{
|
||||
output[i] = _imdctWindow[i] * dctOut[i + half] + _imdctPrevious[i];
|
||||
output[i + half] = _imdctWindow[i + half] * -dctOut[size - 1 - i] - _imdctPrevious[i + half];
|
||||
_imdctPrevious[i] = _imdctWindow[size - 1 - i] * -dctOut[half - i - 1];
|
||||
_imdctPrevious[i + half] = _imdctWindow[half - i - 1] * dctOut[i];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Does a Type-4 DCT.
|
||||
/// </summary>
|
||||
/// <param name="input">The input array containing the time or frequency-domain samples</param>
|
||||
/// <param name="output">The output array that will contain the transformed time or frequency-domain samples</param>
|
||||
private void Dct4(double[] input, double[] output)
|
||||
{
|
||||
int[] shuffleTable = ShuffleTables[MdctBits];
|
||||
double[] sinTable = SinTables[MdctBits];
|
||||
double[] cosTable = CosTables[MdctBits];
|
||||
double[] dctTemp = _scratchDct;
|
||||
|
||||
int size = MdctSize;
|
||||
int lastIndex = size - 1;
|
||||
int halfSize = size / 2;
|
||||
|
||||
for (int i = 0; i < halfSize; i++)
|
||||
{
|
||||
int i2 = i * 2;
|
||||
double a = input[i2];
|
||||
double b = input[lastIndex - i2];
|
||||
double sin = sinTable[i];
|
||||
double cos = cosTable[i];
|
||||
dctTemp[i2] = a * cos + b * sin;
|
||||
dctTemp[i2 + 1] = a * sin - b * cos;
|
||||
}
|
||||
int stageCount = MdctBits - 1;
|
||||
|
||||
for (int stage = 0; stage < stageCount; stage++)
|
||||
{
|
||||
int blockCount = 1 << stage;
|
||||
int blockSizeBits = stageCount - stage;
|
||||
int blockHalfSizeBits = blockSizeBits - 1;
|
||||
int blockSize = 1 << blockSizeBits;
|
||||
int blockHalfSize = 1 << blockHalfSizeBits;
|
||||
sinTable = SinTables[blockHalfSizeBits];
|
||||
cosTable = CosTables[blockHalfSizeBits];
|
||||
|
||||
for (int block = 0; block < blockCount; block++)
|
||||
{
|
||||
for (int i = 0; i < blockHalfSize; i++)
|
||||
{
|
||||
int frontPos = (block * blockSize + i) * 2;
|
||||
int backPos = frontPos + blockSize;
|
||||
double a = dctTemp[frontPos] - dctTemp[backPos];
|
||||
double b = dctTemp[frontPos + 1] - dctTemp[backPos + 1];
|
||||
double sin = sinTable[i];
|
||||
double cos = cosTable[i];
|
||||
dctTemp[frontPos] += dctTemp[backPos];
|
||||
dctTemp[frontPos + 1] += dctTemp[backPos + 1];
|
||||
dctTemp[backPos] = a * cos + b * sin;
|
||||
dctTemp[backPos + 1] = a * sin - b * cos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for (int i = 0; i < MdctSize; i++)
|
||||
{
|
||||
output[i] = dctTemp[shuffleTable[i]] * Scale;
|
||||
}
|
||||
}
|
||||
|
||||
internal static void GenerateTrigTables(int sizeBits, out double[] sin, out double[] cos)
|
||||
{
|
||||
int size = 1 << sizeBits;
|
||||
sin = new double[size];
|
||||
cos = new double[size];
|
||||
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
double value = Math.PI * (4 * i + 1) / (4 * size);
|
||||
sin[i] = Math.Sin(value);
|
||||
cos[i] = Math.Cos(value);
|
||||
}
|
||||
}
|
||||
|
||||
internal static int[] GenerateShuffleTable(int sizeBits)
|
||||
{
|
||||
int size = 1 << sizeBits;
|
||||
var table = new int[size];
|
||||
|
||||
for (int i = 0; i < size; i++)
|
||||
{
|
||||
table[i] = Bit.BitReverse32(i ^ (i / 2), sizeBits);
|
||||
}
|
||||
|
||||
return table;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,162 @@
|
||||
// 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)
|
||||
{
|
||||
var loc = Localization.Instance;
|
||||
|
||||
_sourceLines = lines;
|
||||
Title = loc.Get("Console.WindowTitle");
|
||||
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 = loc.Get("Console.SearchWatermark"),
|
||||
Width = 320,
|
||||
Margin = new Thickness(0, 0, 12, 0),
|
||||
};
|
||||
_autoScrollCheck = new CheckBox
|
||||
{
|
||||
Content = loc.Get("Console.AutoScroll"),
|
||||
IsChecked = autoScroll,
|
||||
FontSize = 12,
|
||||
Margin = new Thickness(0, 0, 12, 0),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
};
|
||||
var copyButton = new Button
|
||||
{
|
||||
Classes = { "ghost" },
|
||||
Content = loc.Get("Console.Copy"),
|
||||
Padding = new Thickness(10, 4),
|
||||
Margin = new Thickness(0, 0, 8, 0),
|
||||
};
|
||||
var clearButton = new Button
|
||||
{
|
||||
Classes = { "ghost" },
|
||||
Content = loc.Get("Console.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 = loc.Get("Console.Title"),
|
||||
VerticalAlignment = VerticalAlignment.Center,
|
||||
},
|
||||
_searchBox.WithGridColumn(1),
|
||||
_autoScrollCheck.WithGridColumn(2),
|
||||
copyButton.WithGridColumn(3),
|
||||
clearButton.WithGridColumn(4),
|
||||
},
|
||||
},
|
||||
_list.WithGridRow(1),
|
||||
},
|
||||
};
|
||||
|
||||
lines.CollectionChanged += OnLinesChanged;
|
||||
Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged;
|
||||
RefreshVisibleLines();
|
||||
}
|
||||
|
||||
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e)
|
||||
{
|
||||
RefreshVisibleLines();
|
||||
if (_autoScrollCheck.IsChecked == true)
|
||||
{
|
||||
Dispatcher.UIThread.Post(() => (_list.Scroll as ScrollViewer)?.ScrollToEnd());
|
||||
}
|
||||
}
|
||||
|
||||
private void RefreshVisibleLines()
|
||||
{
|
||||
var query = _searchBox.Text ?? string.Empty;
|
||||
_visibleLines.Clear();
|
||||
_visibleLines.AddRange(string.IsNullOrWhiteSpace(query)
|
||||
? _sourceLines
|
||||
: _sourceLines.Where(line => line.Text.Contains(query, StringComparison.OrdinalIgnoreCase)));
|
||||
}
|
||||
|
||||
private async Task CopyAsync()
|
||||
{
|
||||
if (_visibleLines.Count == 0 || Clipboard is null)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
await Clipboard.SetTextAsync(string.Join(Environment.NewLine, _visibleLines.Select(line => line.Text)));
|
||||
}
|
||||
}
|
||||
|
||||
file static class GridExtensions
|
||||
{
|
||||
public static T WithGridColumn<T>(this T control, int column) where T : Control
|
||||
{
|
||||
Grid.SetColumn(control, column);
|
||||
return control;
|
||||
}
|
||||
|
||||
public static T WithGridRow<T>(this T control, int row) where T : Control
|
||||
{
|
||||
Grid.SetRow(control, row);
|
||||
return control;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,252 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.IO.Pipes;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Minimal Discord Rich Presence client. Speaks Discord's local IPC
|
||||
/// protocol (framed JSON over the discord-ipc-N named pipe) directly, so no
|
||||
/// external dependency is needed. All failures are silent: no Discord, no
|
||||
/// presence, no impact on the launcher.
|
||||
/// </summary>
|
||||
public sealed class DiscordRichPresence : IDisposable
|
||||
{
|
||||
private const int OpHandshake = 0;
|
||||
private const int OpFrame = 1;
|
||||
private const int MaxFrameBytes = 64 * 1024;
|
||||
|
||||
private static readonly JsonSerializerOptions JsonOptions = new()
|
||||
{
|
||||
DefaultIgnoreCondition = System.Text.Json.Serialization.JsonIgnoreCondition.WhenWritingNull,
|
||||
};
|
||||
|
||||
private readonly object _gate = new();
|
||||
private readonly string _clientId;
|
||||
private readonly AutoResetEvent _signal = new(false);
|
||||
private readonly Thread _worker;
|
||||
|
||||
private NamedPipeClientStream? _pipe;
|
||||
private PresenceState? _desired;
|
||||
private PresenceState? _sent;
|
||||
private volatile bool _disposed;
|
||||
|
||||
private sealed record PresenceState(string Details, string? State, long? StartUnixSeconds);
|
||||
|
||||
public DiscordRichPresence(string clientId)
|
||||
{
|
||||
_clientId = clientId;
|
||||
_worker = new Thread(WorkerLoop)
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "DiscordRichPresence",
|
||||
};
|
||||
_worker.Start();
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Requests the given presence; the worker applies it asynchronously and
|
||||
/// retries (with reconnection) until it is delivered or replaced.
|
||||
/// </summary>
|
||||
public void SetPresence(string details, string? state = null, long? startUnixSeconds = null)
|
||||
{
|
||||
lock (_gate)
|
||||
{
|
||||
_desired = new PresenceState(details, state, startUnixSeconds);
|
||||
}
|
||||
|
||||
Trace($"queued details=\"{details}\" state=\"{state}\"");
|
||||
_signal.Set();
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
_disposed = true;
|
||||
_signal.Set();
|
||||
ClosePipe();
|
||||
}
|
||||
|
||||
private void WorkerLoop()
|
||||
{
|
||||
while (!_disposed)
|
||||
{
|
||||
// Woken immediately on presence changes; the periodic tick
|
||||
// retries after connection failures (e.g. Discord starts later).
|
||||
_signal.WaitOne(TimeSpan.FromSeconds(15));
|
||||
if (_disposed || _clientId.Length == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
PresenceState? desired;
|
||||
lock (_gate)
|
||||
{
|
||||
desired = _desired;
|
||||
}
|
||||
|
||||
if (desired is null || desired == _sent)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
EnsureConnected();
|
||||
SendActivity(desired);
|
||||
_sent = desired;
|
||||
Trace($"sent details=\"{desired.Details}\" state=\"{desired.State}\"");
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
// Connection died (or never existed); reconnect on a later tick.
|
||||
Trace($"send failed: {exception.Message}");
|
||||
ClosePipe();
|
||||
_sent = null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void EnsureConnected()
|
||||
{
|
||||
if (_pipe is { IsConnected: true })
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
ClosePipe();
|
||||
Exception? lastError = null;
|
||||
for (var index = 0; index < 10; index++)
|
||||
{
|
||||
var pipe = new NamedPipeClientStream(
|
||||
".",
|
||||
$"discord-ipc-{index}",
|
||||
PipeDirection.InOut,
|
||||
PipeOptions.Asynchronous);
|
||||
try
|
||||
{
|
||||
pipe.Connect(500);
|
||||
_pipe = pipe;
|
||||
WriteFrame(OpHandshake, JsonSerializer.Serialize(new
|
||||
{
|
||||
v = 1,
|
||||
client_id = _clientId,
|
||||
}));
|
||||
|
||||
// Discord replies with a READY (or error) frame; consume it
|
||||
// so the pipe buffer stays drained.
|
||||
_ = ReadFrame();
|
||||
return;
|
||||
}
|
||||
catch (Exception exception)
|
||||
{
|
||||
lastError = exception;
|
||||
pipe.Dispose();
|
||||
_pipe = null;
|
||||
}
|
||||
}
|
||||
|
||||
throw lastError ?? new IOException("Discord IPC pipe not found.");
|
||||
}
|
||||
|
||||
private void SendActivity(PresenceState presence)
|
||||
{
|
||||
var payload = new Dictionary<string, object?>
|
||||
{
|
||||
["cmd"] = "SET_ACTIVITY",
|
||||
["nonce"] = Guid.NewGuid().ToString(),
|
||||
["args"] = new Dictionary<string, object?>
|
||||
{
|
||||
["pid"] = Environment.ProcessId,
|
||||
["activity"] = new Dictionary<string, object?>
|
||||
{
|
||||
["details"] = presence.Details,
|
||||
["state"] = presence.State,
|
||||
["timestamps"] = presence.StartUnixSeconds is { } start
|
||||
? new Dictionary<string, object?> { ["start"] = start }
|
||||
: null,
|
||||
["assets"] = new Dictionary<string, object?>
|
||||
{
|
||||
["large_image"] = "logo",
|
||||
["large_text"] = "SharpEmu",
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
WriteFrame(OpFrame, JsonSerializer.Serialize(payload, JsonOptions));
|
||||
|
||||
// Consume Discord's acknowledgement to keep the pipe drained.
|
||||
_ = ReadFrame();
|
||||
}
|
||||
|
||||
private void WriteFrame(int opcode, string json)
|
||||
{
|
||||
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
|
||||
var payload = Encoding.UTF8.GetBytes(json);
|
||||
var frame = new byte[8 + payload.Length];
|
||||
BitConverter.TryWriteBytes(frame.AsSpan(0, 4), opcode);
|
||||
BitConverter.TryWriteBytes(frame.AsSpan(4, 4), payload.Length);
|
||||
payload.CopyTo(frame, 8);
|
||||
pipe.Write(frame, 0, frame.Length);
|
||||
pipe.Flush();
|
||||
}
|
||||
|
||||
private string ReadFrame()
|
||||
{
|
||||
var pipe = _pipe ?? throw new IOException("Discord IPC pipe is not connected.");
|
||||
var header = ReadExactly(pipe, 8);
|
||||
var length = BitConverter.ToInt32(header, 4);
|
||||
if (length is < 0 or > MaxFrameBytes)
|
||||
{
|
||||
throw new IOException($"Discord IPC frame length {length} is out of range.");
|
||||
}
|
||||
|
||||
return Encoding.UTF8.GetString(ReadExactly(pipe, length));
|
||||
}
|
||||
|
||||
private static byte[] ReadExactly(NamedPipeClientStream pipe, int count)
|
||||
{
|
||||
var buffer = new byte[count];
|
||||
var read = 0;
|
||||
while (read < count)
|
||||
{
|
||||
var chunk = pipe.Read(buffer, read, count - read);
|
||||
if (chunk <= 0)
|
||||
{
|
||||
throw new IOException("Discord IPC pipe closed.");
|
||||
}
|
||||
|
||||
read += chunk;
|
||||
}
|
||||
|
||||
return buffer;
|
||||
}
|
||||
|
||||
private static void Trace(string message)
|
||||
{
|
||||
if (string.Equals(
|
||||
Environment.GetEnvironmentVariable("SHARPEMU_LOG_DISCORD"),
|
||||
"1",
|
||||
StringComparison.Ordinal))
|
||||
{
|
||||
Console.Error.WriteLine($"[DISCORD] {message}");
|
||||
}
|
||||
}
|
||||
|
||||
private void ClosePipe()
|
||||
{
|
||||
var pipe = _pipe;
|
||||
_pipe = null;
|
||||
try
|
||||
{
|
||||
pipe?.Dispose();
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Best effort; the pipe may already be broken.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,637 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.ComponentModel;
|
||||
using System.Diagnostics;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using Microsoft.Win32.SafeHandles;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Launches the SharpEmu CLI as a child process with the same CET/CFG mitigation
|
||||
/// opt-outs the CLI would apply to its own relaunched child, while capturing
|
||||
/// stdout/stderr through pipes. The CLI's internal relaunch is suppressed via
|
||||
/// SHARPEMU_DISABLE_MITIGATION_RELAUNCH so output is not lost to a detached
|
||||
/// console. A kill-on-close job object ties the emulator's lifetime to the GUI.
|
||||
/// </summary>
|
||||
internal sealed class EmulatorProcess : IDisposable
|
||||
{
|
||||
private const uint EXTENDED_STARTUPINFO_PRESENT = 0x00080000;
|
||||
private const uint CREATE_NO_WINDOW = 0x08000000;
|
||||
private const int STARTF_USESTDHANDLES = 0x00000100;
|
||||
private const uint HANDLE_FLAG_INHERIT = 0x00000001;
|
||||
private const uint INFINITE = 0xFFFFFFFF;
|
||||
private const int PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY = 0x00020007;
|
||||
private const uint JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE = 0x00002000;
|
||||
private const int JobObjectExtendedLimitInformation = 9;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
|
||||
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
|
||||
|
||||
private readonly object _sync = new();
|
||||
private nint _processHandle;
|
||||
private nint _jobHandle;
|
||||
private Process? _fallbackProcess;
|
||||
private bool _running;
|
||||
private bool _disposed;
|
||||
|
||||
public event Action<string, bool>? OutputReceived;
|
||||
|
||||
public event Action<int>? Exited;
|
||||
|
||||
public bool IsRunning
|
||||
{
|
||||
get
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
return _running;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Start(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
ObjectDisposedException.ThrowIf(_disposed, this);
|
||||
if (_running)
|
||||
{
|
||||
throw new InvalidOperationException("The emulator process is already running.");
|
||||
}
|
||||
|
||||
if (OperatingSystem.IsWindows())
|
||||
{
|
||||
StartWindows(exePath, arguments, workingDirectory);
|
||||
}
|
||||
else
|
||||
{
|
||||
StartFallback(exePath, arguments, workingDirectory);
|
||||
}
|
||||
|
||||
_running = true;
|
||||
}
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
// Prefer terminating the job: it kills the whole tree, including
|
||||
// any children the emulator spawned, even when the main process
|
||||
// is wedged in a GPU driver call.
|
||||
if (_jobHandle != 0)
|
||||
{
|
||||
_ = TerminateJobObject(_jobHandle, 1);
|
||||
}
|
||||
|
||||
if (_processHandle != 0)
|
||||
{
|
||||
_ = TerminateProcess(_processHandle, 1);
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
_fallbackProcess?.Kill(entireProcessTree: true);
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
// Already exited.
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (_disposed)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_disposed = true;
|
||||
}
|
||||
|
||||
Stop();
|
||||
}
|
||||
|
||||
private void StartWindows(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
|
||||
{
|
||||
// The CLI would otherwise relaunch itself into a mitigated child whose
|
||||
// console output cannot flow through our pipes.
|
||||
Environment.SetEnvironmentVariable("SHARPEMU_DISABLE_MITIGATION_RELAUNCH", "1");
|
||||
|
||||
var securityAttributes = new SECURITY_ATTRIBUTES
|
||||
{
|
||||
nLength = Marshal.SizeOf<SECURITY_ATTRIBUTES>(),
|
||||
bInheritHandle = 1,
|
||||
};
|
||||
|
||||
if (!CreatePipe(out var stdoutRead, out var stdoutWrite, ref securityAttributes, 0) ||
|
||||
!CreatePipe(out var stderrRead, out var stderrWrite, ref securityAttributes, 0))
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to create output pipes.");
|
||||
}
|
||||
|
||||
_ = SetHandleInformation(stdoutRead, HANDLE_FLAG_INHERIT, 0);
|
||||
_ = SetHandleInformation(stderrRead, HANDLE_FLAG_INHERIT, 0);
|
||||
|
||||
var startupInfoEx = new STARTUPINFOEX();
|
||||
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
|
||||
startupInfoEx.StartupInfo.dwFlags = STARTF_USESTDHANDLES;
|
||||
startupInfoEx.StartupInfo.hStdOutput = stdoutWrite;
|
||||
startupInfoEx.StartupInfo.hStdError = stderrWrite;
|
||||
|
||||
nint attributeList = 0;
|
||||
nint mitigationPolicies = 0;
|
||||
try
|
||||
{
|
||||
nuint attributeListSize = 0;
|
||||
_ = InitializeProcThreadAttributeList(0, 1, 0, ref attributeListSize);
|
||||
attributeList = Marshal.AllocHGlobal((nint)attributeListSize);
|
||||
if (!InitializeProcThreadAttributeList(attributeList, 1, 0, ref attributeListSize))
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to initialize the process attribute list.");
|
||||
}
|
||||
|
||||
startupInfoEx.lpAttributeList = attributeList;
|
||||
|
||||
var policy1 = PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF;
|
||||
var policy2 =
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF |
|
||||
PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF;
|
||||
|
||||
mitigationPolicies = Marshal.AllocHGlobal(sizeof(ulong) * 2);
|
||||
Marshal.WriteInt64(mitigationPolicies, unchecked((long)policy1));
|
||||
Marshal.WriteInt64(nint.Add(mitigationPolicies, sizeof(long)), unchecked((long)policy2));
|
||||
|
||||
if (!UpdateProcThreadAttribute(
|
||||
attributeList,
|
||||
0,
|
||||
PROC_THREAD_ATTRIBUTE_MITIGATION_POLICY,
|
||||
mitigationPolicies,
|
||||
(nuint)(sizeof(ulong) * 2),
|
||||
0,
|
||||
0))
|
||||
{
|
||||
throw new Win32Exception(Marshal.GetLastWin32Error(), "Failed to apply the mitigation policy.");
|
||||
}
|
||||
|
||||
var currentDirectory = workingDirectory ?? Environment.CurrentDirectory;
|
||||
var created = CreateProcessW(
|
||||
exePath,
|
||||
new StringBuilder(BuildCommandLine(exePath, arguments)),
|
||||
0,
|
||||
0,
|
||||
true,
|
||||
EXTENDED_STARTUPINFO_PRESENT | CREATE_NO_WINDOW,
|
||||
0,
|
||||
currentDirectory,
|
||||
ref startupInfoEx,
|
||||
out var processInfo);
|
||||
|
||||
if (!created)
|
||||
{
|
||||
var error = Marshal.GetLastWin32Error();
|
||||
throw new Win32Exception(error, $"Failed to start '{exePath}' with CET/CFG mitigation disabled (Win32 error {error}: {new Win32Exception(error).Message}).");
|
||||
}
|
||||
|
||||
CloseHandle(processInfo.hThread);
|
||||
_processHandle = processInfo.hProcess;
|
||||
|
||||
_jobHandle = CreateJobObjectW(0, null);
|
||||
if (_jobHandle != 0 &&
|
||||
(!TryEnableKillOnJobClose(_jobHandle) || !AssignProcessToJobObject(_jobHandle, processInfo.hProcess)))
|
||||
{
|
||||
CloseHandle(_jobHandle);
|
||||
_jobHandle = 0;
|
||||
}
|
||||
|
||||
StartReaderThread(stdoutRead, isError: false);
|
||||
StartReaderThread(stderrRead, isError: true);
|
||||
StartExitWatcherThread();
|
||||
}
|
||||
catch
|
||||
{
|
||||
CloseHandle(stdoutRead);
|
||||
CloseHandle(stderrRead);
|
||||
throw;
|
||||
}
|
||||
finally
|
||||
{
|
||||
// The child owns duplicated pipe write ends; closing ours lets the
|
||||
// readers observe EOF when the child exits.
|
||||
CloseHandle(stdoutWrite);
|
||||
CloseHandle(stderrWrite);
|
||||
|
||||
if (attributeList != 0)
|
||||
{
|
||||
DeleteProcThreadAttributeList(attributeList);
|
||||
Marshal.FreeHGlobal(attributeList);
|
||||
}
|
||||
|
||||
if (mitigationPolicies != 0)
|
||||
{
|
||||
Marshal.FreeHGlobal(mitigationPolicies);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void StartFallback(string exePath, IReadOnlyList<string> arguments, string? workingDirectory)
|
||||
{
|
||||
var startInfo = new ProcessStartInfo
|
||||
{
|
||||
FileName = exePath,
|
||||
WorkingDirectory = workingDirectory ?? Environment.CurrentDirectory,
|
||||
UseShellExecute = false,
|
||||
CreateNoWindow = true,
|
||||
RedirectStandardOutput = true,
|
||||
RedirectStandardError = true,
|
||||
};
|
||||
foreach (var argument in arguments)
|
||||
{
|
||||
startInfo.ArgumentList.Add(argument);
|
||||
}
|
||||
|
||||
var process = new Process { StartInfo = startInfo, EnableRaisingEvents = true };
|
||||
process.OutputDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data is not null)
|
||||
{
|
||||
OutputReceived?.Invoke(e.Data, false);
|
||||
}
|
||||
};
|
||||
process.ErrorDataReceived += (_, e) =>
|
||||
{
|
||||
if (e.Data is not null)
|
||||
{
|
||||
OutputReceived?.Invoke(e.Data, true);
|
||||
}
|
||||
};
|
||||
process.Exited += (_, _) =>
|
||||
{
|
||||
int exitCode;
|
||||
try
|
||||
{
|
||||
exitCode = process.ExitCode;
|
||||
}
|
||||
catch (InvalidOperationException)
|
||||
{
|
||||
exitCode = -1;
|
||||
}
|
||||
|
||||
OnExited(exitCode);
|
||||
};
|
||||
|
||||
process.Start();
|
||||
process.BeginOutputReadLine();
|
||||
process.BeginErrorReadLine();
|
||||
_fallbackProcess = process;
|
||||
}
|
||||
|
||||
private void StartReaderThread(nint readHandle, bool isError)
|
||||
{
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
using var stream = new FileStream(new SafeFileHandle(readHandle, ownsHandle: true), FileAccess.Read);
|
||||
using var reader = new StreamReader(stream, Encoding.UTF8);
|
||||
try
|
||||
{
|
||||
while (reader.ReadLine() is { } line)
|
||||
{
|
||||
OutputReceived?.Invoke(line, isError);
|
||||
}
|
||||
}
|
||||
catch (IOException)
|
||||
{
|
||||
// Pipe broken on process teardown.
|
||||
}
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = isError ? "SharpEmu stderr reader" : "SharpEmu stdout reader",
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private void StartExitWatcherThread()
|
||||
{
|
||||
var processHandle = _processHandle;
|
||||
var thread = new Thread(() =>
|
||||
{
|
||||
_ = WaitForSingleObject(processHandle, INFINITE);
|
||||
var exitCode = GetExitCodeProcess(processHandle, out var rawExitCode)
|
||||
? unchecked((int)rawExitCode)
|
||||
: -1;
|
||||
OnExited(exitCode);
|
||||
})
|
||||
{
|
||||
IsBackground = true,
|
||||
Name = "SharpEmu exit watcher",
|
||||
};
|
||||
thread.Start();
|
||||
}
|
||||
|
||||
private void OnExited(int exitCode)
|
||||
{
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_running)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_running = false;
|
||||
if (_processHandle != 0)
|
||||
{
|
||||
CloseHandle(_processHandle);
|
||||
_processHandle = 0;
|
||||
}
|
||||
|
||||
if (_jobHandle != 0)
|
||||
{
|
||||
CloseHandle(_jobHandle);
|
||||
_jobHandle = 0;
|
||||
}
|
||||
|
||||
_fallbackProcess?.Dispose();
|
||||
_fallbackProcess = null;
|
||||
}
|
||||
|
||||
Exited?.Invoke(exitCode);
|
||||
}
|
||||
|
||||
private static bool TryEnableKillOnJobClose(nint jobHandle)
|
||||
{
|
||||
var extendedLimitInfo = new JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||
{
|
||||
BasicLimitInformation = new JOBOBJECT_BASIC_LIMIT_INFORMATION
|
||||
{
|
||||
LimitFlags = JOB_OBJECT_LIMIT_KILL_ON_JOB_CLOSE,
|
||||
},
|
||||
};
|
||||
|
||||
var size = Marshal.SizeOf<JOBOBJECT_EXTENDED_LIMIT_INFORMATION>();
|
||||
var memory = Marshal.AllocHGlobal(size);
|
||||
try
|
||||
{
|
||||
Marshal.StructureToPtr(extendedLimitInfo, memory, false);
|
||||
return SetInformationJobObject(
|
||||
jobHandle,
|
||||
JobObjectExtendedLimitInformation,
|
||||
memory,
|
||||
unchecked((uint)size));
|
||||
}
|
||||
finally
|
||||
{
|
||||
Marshal.FreeHGlobal(memory);
|
||||
}
|
||||
}
|
||||
|
||||
private static string BuildCommandLine(string processPath, IReadOnlyList<string> args)
|
||||
{
|
||||
var builder = new StringBuilder();
|
||||
builder.Append(QuoteArgument(processPath));
|
||||
for (var i = 0; i < args.Count; i++)
|
||||
{
|
||||
builder.Append(' ');
|
||||
builder.Append(QuoteArgument(args[i]));
|
||||
}
|
||||
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
private static string QuoteArgument(string argument)
|
||||
{
|
||||
if (argument.Length == 0)
|
||||
{
|
||||
return "\"\"";
|
||||
}
|
||||
|
||||
var needsQuotes = false;
|
||||
foreach (var c in argument)
|
||||
{
|
||||
if (char.IsWhiteSpace(c) || c == '"')
|
||||
{
|
||||
needsQuotes = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if (!needsQuotes)
|
||||
{
|
||||
return argument;
|
||||
}
|
||||
|
||||
var builder = new StringBuilder(argument.Length + 2);
|
||||
builder.Append('"');
|
||||
|
||||
var backslashCount = 0;
|
||||
foreach (var c in argument)
|
||||
{
|
||||
if (c == '\\')
|
||||
{
|
||||
backslashCount++;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (c == '"')
|
||||
{
|
||||
builder.Append('\\', (backslashCount * 2) + 1);
|
||||
builder.Append('"');
|
||||
backslashCount = 0;
|
||||
continue;
|
||||
}
|
||||
|
||||
if (backslashCount > 0)
|
||||
{
|
||||
builder.Append('\\', backslashCount);
|
||||
backslashCount = 0;
|
||||
}
|
||||
|
||||
builder.Append(c);
|
||||
}
|
||||
|
||||
if (backslashCount > 0)
|
||||
{
|
||||
builder.Append('\\', backslashCount * 2);
|
||||
}
|
||||
|
||||
builder.Append('"');
|
||||
return builder.ToString();
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct SECURITY_ATTRIBUTES
|
||||
{
|
||||
public int nLength;
|
||||
public nint lpSecurityDescriptor;
|
||||
public int bInheritHandle;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
|
||||
private struct STARTUPINFO
|
||||
{
|
||||
public int cb;
|
||||
public nint lpReserved;
|
||||
public nint lpDesktop;
|
||||
public nint lpTitle;
|
||||
public int dwX;
|
||||
public int dwY;
|
||||
public int dwXSize;
|
||||
public int dwYSize;
|
||||
public int dwXCountChars;
|
||||
public int dwYCountChars;
|
||||
public int dwFillAttribute;
|
||||
public int dwFlags;
|
||||
public short wShowWindow;
|
||||
public short cbReserved2;
|
||||
public nint lpReserved2;
|
||||
public nint hStdInput;
|
||||
public nint hStdOutput;
|
||||
public nint hStdError;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct STARTUPINFOEX
|
||||
{
|
||||
public STARTUPINFO StartupInfo;
|
||||
public nint lpAttributeList;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct PROCESS_INFORMATION
|
||||
{
|
||||
public nint hProcess;
|
||||
public nint hThread;
|
||||
public int dwProcessId;
|
||||
public int dwThreadId;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct JOBOBJECT_BASIC_LIMIT_INFORMATION
|
||||
{
|
||||
public long PerProcessUserTimeLimit;
|
||||
public long PerJobUserTimeLimit;
|
||||
public uint LimitFlags;
|
||||
public nuint MinimumWorkingSetSize;
|
||||
public nuint MaximumWorkingSetSize;
|
||||
public uint ActiveProcessLimit;
|
||||
public nint Affinity;
|
||||
public uint PriorityClass;
|
||||
public uint SchedulingClass;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct IO_COUNTERS
|
||||
{
|
||||
public ulong ReadOperationCount;
|
||||
public ulong WriteOperationCount;
|
||||
public ulong OtherOperationCount;
|
||||
public ulong ReadTransferCount;
|
||||
public ulong WriteTransferCount;
|
||||
public ulong OtherTransferCount;
|
||||
}
|
||||
|
||||
[StructLayout(LayoutKind.Sequential)]
|
||||
private struct JOBOBJECT_EXTENDED_LIMIT_INFORMATION
|
||||
{
|
||||
public JOBOBJECT_BASIC_LIMIT_INFORMATION BasicLimitInformation;
|
||||
public IO_COUNTERS IoInfo;
|
||||
public nuint ProcessMemoryLimit;
|
||||
public nuint JobMemoryLimit;
|
||||
public nuint PeakProcessMemoryUsed;
|
||||
public nuint PeakJobMemoryUsed;
|
||||
}
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CreatePipe(
|
||||
out nint hReadPipe,
|
||||
out nint hWritePipe,
|
||||
ref SECURITY_ATTRIBUTES lpPipeAttributes,
|
||||
uint nSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetHandleInformation(nint hObject, uint dwMask, uint dwFlags);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool InitializeProcThreadAttributeList(
|
||||
nint lpAttributeList,
|
||||
int dwAttributeCount,
|
||||
int dwFlags,
|
||||
ref nuint lpSize);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool UpdateProcThreadAttribute(
|
||||
nint lpAttributeList,
|
||||
uint dwFlags,
|
||||
nint attribute,
|
||||
nint lpValue,
|
||||
nuint cbSize,
|
||||
nint lpPreviousValue,
|
||||
nint lpReturnSize);
|
||||
|
||||
[DllImport("kernel32.dll")]
|
||||
private static extern void DeleteProcThreadAttributeList(nint lpAttributeList);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "CreateJobObjectW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
private static extern nint CreateJobObjectW(nint lpJobAttributes, string? lpName);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool SetInformationJobObject(
|
||||
nint hJob,
|
||||
int jobObjectInfoClass,
|
||||
nint lpJobObjectInfo,
|
||||
uint cbJobObjectInfoLength);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool AssignProcessToJobObject(nint hJob, nint hProcess);
|
||||
|
||||
[DllImport("kernel32.dll", EntryPoint = "CreateProcessW", SetLastError = true, CharSet = CharSet.Unicode)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CreateProcessW(
|
||||
string applicationName,
|
||||
StringBuilder commandLine,
|
||||
nint processAttributes,
|
||||
nint threadAttributes,
|
||||
[MarshalAs(UnmanagedType.Bool)] bool inheritHandles,
|
||||
uint creationFlags,
|
||||
nint environment,
|
||||
string currentDirectory,
|
||||
ref STARTUPINFOEX startupInfo,
|
||||
out PROCESS_INFORMATION processInformation);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
private static extern uint WaitForSingleObject(nint handle, uint milliseconds);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool GetExitCodeProcess(nint process, out uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool TerminateProcess(nint process, uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool TerminateJobObject(nint job, uint exitCode);
|
||||
|
||||
[DllImport("kernel32.dll", SetLastError = true)]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool CloseHandle(nint handle);
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.ComponentModel;
|
||||
using Avalonia;
|
||||
using Avalonia.Media;
|
||||
using Avalonia.Media.Imaging;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public sealed class GameEntry : INotifyPropertyChanged
|
||||
{
|
||||
// Placeholder gradients for games without cover art, picked
|
||||
// deterministically from the game name so a game keeps its color.
|
||||
private static readonly (Color Start, Color End)[] PlaceholderPalette =
|
||||
{
|
||||
(Color.Parse("#5B4B8A"), Color.Parse("#2C2A4A")),
|
||||
(Color.Parse("#1F6E8C"), Color.Parse("#173B45")),
|
||||
(Color.Parse("#7A4069"), Color.Parse("#3B1C32")),
|
||||
(Color.Parse("#2D6A4F"), Color.Parse("#1B3A2B")),
|
||||
(Color.Parse("#8C5425"), Color.Parse("#4A2B12")),
|
||||
(Color.Parse("#4F6D9E"), Color.Parse("#263349")),
|
||||
(Color.Parse("#8A4B4B"), Color.Parse("#3F2222")),
|
||||
(Color.Parse("#3E7C7B"), Color.Parse("#1E3D3C")),
|
||||
};
|
||||
|
||||
private Bitmap? _cover;
|
||||
private IBrush? _placeholderBrush;
|
||||
private long _sizeBytes;
|
||||
|
||||
public GameEntry(
|
||||
string name, string? titleId, string? version, string path, long sizeBytes,
|
||||
string? coverPath, string? backgroundPath)
|
||||
{
|
||||
Name = name;
|
||||
TitleId = titleId;
|
||||
Version = version;
|
||||
Path = path;
|
||||
_sizeBytes = sizeBytes;
|
||||
CoverPath = coverPath;
|
||||
BackgroundPath = backgroundPath;
|
||||
Initials = ComputeInitials(name);
|
||||
}
|
||||
|
||||
public event PropertyChangedEventHandler? PropertyChanged;
|
||||
|
||||
public string Name { get; }
|
||||
|
||||
public string? TitleId { get; }
|
||||
|
||||
/// <summary>Content version from sce_sys/param.json, e.g. "01.000.000".</summary>
|
||||
public string? Version { get; }
|
||||
|
||||
public string Path { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Total size of the game. Initially the eboot's own size from the scan;
|
||||
/// replaced with the full install folder size once computed in the
|
||||
/// background.
|
||||
/// </summary>
|
||||
public long SizeBytes
|
||||
{
|
||||
get => _sizeBytes;
|
||||
set
|
||||
{
|
||||
if (_sizeBytes == value)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_sizeBytes = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeBytes)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(SizeText)));
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Path to the cover art image shipped with the game, if found.</summary>
|
||||
public string? CoverPath { get; }
|
||||
|
||||
/// <summary>Path to the key art (pic0/pic1) shipped with the game, if found.</summary>
|
||||
public string? BackgroundPath { get; }
|
||||
|
||||
/// <summary>
|
||||
/// Decoded key art used as the window backdrop while this game is
|
||||
/// selected. Loaded on demand and cached; not exposed via binding.
|
||||
/// </summary>
|
||||
public Bitmap? Background { get; set; }
|
||||
|
||||
public string Initials { get; }
|
||||
|
||||
// Built lazily: brushes are AvaloniaObjects that must be created on the
|
||||
// UI thread, while GameEntry itself is constructed on the scan thread.
|
||||
public IBrush PlaceholderBrush => _placeholderBrush ??= BuildPlaceholderBrush(Name);
|
||||
|
||||
/// <summary>Decoded cover art; loaded asynchronously after the library scan.</summary>
|
||||
public Bitmap? Cover
|
||||
{
|
||||
get => _cover;
|
||||
set
|
||||
{
|
||||
if (ReferenceEquals(_cover, value))
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cover = value;
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Cover)));
|
||||
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(HasCover)));
|
||||
}
|
||||
}
|
||||
|
||||
public bool HasCover => _cover is not null;
|
||||
|
||||
public bool HasTitleId => TitleId is not null;
|
||||
|
||||
/// <summary>Badge text shown in the launch bar, e.g. "v01.000.000".</summary>
|
||||
public string? VersionText => Version is null ? null : $"v{Version}";
|
||||
|
||||
public bool HasVersion => Version is not null;
|
||||
|
||||
/// <summary>Formatted install size badge shown in the launch bar.</summary>
|
||||
public string SizeText => FormatSize(SizeBytes);
|
||||
|
||||
private static string ComputeInitials(string name)
|
||||
{
|
||||
var initials = name
|
||||
.Split(' ', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries)
|
||||
.Where(word => char.IsLetterOrDigit(word[0]))
|
||||
.Select(word => char.ToUpperInvariant(word[0]))
|
||||
.Take(2)
|
||||
.ToArray();
|
||||
|
||||
return initials.Length > 0 ? new string(initials) : "?";
|
||||
}
|
||||
|
||||
private static IBrush BuildPlaceholderBrush(string name)
|
||||
{
|
||||
var hash = 0;
|
||||
foreach (var ch in name)
|
||||
{
|
||||
hash = unchecked(hash * 31 + ch);
|
||||
}
|
||||
|
||||
var (start, end) = PlaceholderPalette[(int)((uint)hash % PlaceholderPalette.Length)];
|
||||
return new LinearGradientBrush
|
||||
{
|
||||
StartPoint = new RelativePoint(0, 0, RelativeUnit.Relative),
|
||||
EndPoint = new RelativePoint(1, 1, RelativeUnit.Relative),
|
||||
GradientStops =
|
||||
{
|
||||
new GradientStop(start, 0),
|
||||
new GradientStop(end, 1),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
private static string FormatSize(long bytes)
|
||||
{
|
||||
return bytes switch
|
||||
{
|
||||
>= 1L << 30 => $"{bytes / (double)(1L << 30):0.0} GiB",
|
||||
>= 1L << 20 => $"{bytes / (double)(1L << 20):0.0} MiB",
|
||||
>= 1L << 10 => $"{bytes / (double)(1L << 10):0.0} KiB",
|
||||
_ => $"{bytes} B",
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Avalonia;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Entry point for the desktop frontend, hosted by the SharpEmu executable
|
||||
/// when it is started without command-line arguments.
|
||||
/// </summary>
|
||||
public static class GuiLauncher
|
||||
{
|
||||
public static int Run()
|
||||
{
|
||||
try
|
||||
{
|
||||
BuildAvaloniaApp().StartWithClassicDesktopLifetime(Array.Empty<string>());
|
||||
return 0;
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
WriteCrashLog(ex);
|
||||
throw;
|
||||
}
|
||||
}
|
||||
|
||||
public static AppBuilder BuildAvaloniaApp()
|
||||
=> AppBuilder.Configure<App>()
|
||||
.UsePlatformDetect()
|
||||
.WithInterFont()
|
||||
.LogToTrace();
|
||||
|
||||
private static void WriteCrashLog(Exception ex)
|
||||
{
|
||||
try
|
||||
{
|
||||
File.AppendAllText(
|
||||
Path.Combine(AppContext.BaseDirectory, "gui-crash.log"),
|
||||
$"[{DateTime.Now:yyyy-MM-dd HH:mm:ss}] {ex}{Environment.NewLine}{Environment.NewLine}");
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Crash logging is best-effort.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,97 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public sealed class GuiSettings
|
||||
{
|
||||
private static readonly JsonSerializerOptions SerializerOptions = new()
|
||||
{
|
||||
WriteIndented = true,
|
||||
};
|
||||
|
||||
public List<string> GameFolders { get; set; } = new();
|
||||
|
||||
/// <summary>Eboot paths hidden from the library via "Remove from library".</summary>
|
||||
public List<string> ExcludedGames { get; set; } = new();
|
||||
|
||||
public string LogLevel { get; set; } = "Info";
|
||||
|
||||
public int ImportTraceLimit { get; set; }
|
||||
|
||||
public bool StrictDynlibResolution { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// Mirror emulator output to user/logs/<titleId>-<timestamp>.log, if <see cref="LogFilePath"/> is null.
|
||||
/// </summary>
|
||||
public bool LogToFile { get; set; }
|
||||
|
||||
/// <summary>If <see cref="LogToFile"/> is true it logs to this file path.</summary>
|
||||
public string? LogFilePath { get; set; }
|
||||
|
||||
/// <summary>
|
||||
/// If <see cref="OverrideLogFile"/> is false it appends <titleId>-<timestamp> to the filename specified by
|
||||
/// <see cref="LogFilePath"/>. Otherwise it uses the exact filename from <see cref="LogFilePath"/>
|
||||
/// </summary>
|
||||
public bool OverrideLogFile { get; set; }
|
||||
|
||||
/// <summary>Loop the selected game's sce_sys/snd0.at9 preview music.</summary>
|
||||
public bool PlayTitleMusic { get; set; } = true;
|
||||
|
||||
public string? EmulatorPath { get; set; }
|
||||
|
||||
/// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
|
||||
public string Language { get; set; } = "en";
|
||||
|
||||
/// <summary>Publish launcher/game status to Discord Rich Presence.</summary>
|
||||
public bool DiscordRichPresence { get; set; } = true;
|
||||
|
||||
/// <summary>
|
||||
/// Discord application ID used for Rich Presence; the default is the
|
||||
/// SharpEmu application. Override to rebrand what Discord shows as
|
||||
/// "Playing …" (register at discord.com/developers/applications).
|
||||
/// </summary>
|
||||
public string DiscordClientId { get; set; } = "1525606762248540221";
|
||||
|
||||
// The emulator is portable and keeps its data next to the executable;
|
||||
// the GUI follows the same convention.
|
||||
public static string SettingsPath => Path.Combine(AppContext.BaseDirectory, "gui-settings.json");
|
||||
|
||||
public static GuiSettings Load()
|
||||
{
|
||||
try
|
||||
{
|
||||
if (File.Exists(SettingsPath))
|
||||
{
|
||||
var json = File.ReadAllText(SettingsPath);
|
||||
return JsonSerializer.Deserialize<GuiSettings>(json, SerializerOptions) ?? new GuiSettings();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Corrupt or unreadable settings fall back to defaults.
|
||||
}
|
||||
|
||||
return new GuiSettings();
|
||||
}
|
||||
|
||||
public void Save()
|
||||
{
|
||||
try
|
||||
{
|
||||
var directory = Path.GetDirectoryName(SettingsPath);
|
||||
if (!string.IsNullOrEmpty(directory))
|
||||
{
|
||||
Directory.CreateDirectory(directory);
|
||||
}
|
||||
|
||||
File.WriteAllText(SettingsPath, JsonSerializer.Serialize(this, SerializerOptions));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Settings persistence is best-effort.
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "العربية",
|
||||
|
||||
"Page.Library": "المكتبة",
|
||||
"Page.Options": "الخيارات",
|
||||
"Page.GameCount.One": "لعبة واحدة",
|
||||
"Page.GameCount.Other": "{0} لعبة",
|
||||
|
||||
"Library.SearchWatermark": "ابحث في المكتبة...",
|
||||
"Library.AddFolder": "+ إضافة مجلد",
|
||||
"Library.Rescan": "⟳ إعادة الفحص",
|
||||
"Library.OpenFile": "فتح ملف...",
|
||||
|
||||
"Library.Context.Launch": "تشغيل",
|
||||
"Library.Context.OpenFolder": "فتح مجلد اللعبة",
|
||||
"Library.Context.CopyPath": "نسخ المسار",
|
||||
"Library.Context.CopyTitleId": "نسخ معرف العنوان",
|
||||
"Library.Context.Remove": "إزالة من المكتبة",
|
||||
|
||||
"Library.Empty.Title": "مكتبتك فارغة",
|
||||
"Library.Empty.Hint": "أضف مجلداً يحتوي على ألعابك للبدء.",
|
||||
"Library.Empty.SearchTitle": "لا توجد ألعاب تطابق بحثك",
|
||||
"Library.Empty.SearchHint": "لا شيء في المكتبة يطابق “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ إضافة مجلد ألعاب",
|
||||
|
||||
"Library.Loading": "جارٍ تحميل المكتبة...",
|
||||
|
||||
"Options.General": "عام",
|
||||
"Options.Section.Emulation": "المحاكاة",
|
||||
"Options.Section.Logging": "التسجيل",
|
||||
"Options.Section.Launcher": "المُشغِّل",
|
||||
|
||||
"Options.CpuEngine.Label": "محرك المعالج",
|
||||
"Options.CpuEngine.Desc": "محرك التنفيذ المستخدم لتشغيل كود اللعبة.",
|
||||
"Options.CpuEngine.Native": "أصلي",
|
||||
|
||||
"Options.Strict.Label": "ربط صارم للمكتبات الديناميكية (dynlib)",
|
||||
"Options.Strict.Desc": "إفشال التشغيل عندما يتعذر التعرف على رمز مستورد.",
|
||||
|
||||
"Options.LogLevel.Label": "مستوى التسجيل",
|
||||
"Options.LogLevel.Desc": "مدى تفصيل مخرجات نافذة سجلات المحاكي.",
|
||||
"Options.LogLevel.Trace": "تتبع",
|
||||
"Options.LogLevel.Debug": "تصحيح الأخطاء",
|
||||
"Options.LogLevel.Info": "معلومات",
|
||||
"Options.LogLevel.Warning": "تحذير",
|
||||
"Options.LogLevel.Error": "خطأ",
|
||||
"Options.LogLevel.Critical": "حرج",
|
||||
|
||||
"Options.TraceImports.Label": "حد تتبع الاستيراد",
|
||||
"Options.TraceImports.Desc": "تتبع أول N استيراد لكل وحدة (0 = إيقاف).",
|
||||
|
||||
"Options.LogToFile.Label": "التسجيل في ملف",
|
||||
"Options.LogToFile.Desc": "نسخ مخرجات المحاكي إلى ملف سجل.",
|
||||
|
||||
"Options.LogFilePath.Label": "مسار ملف السجل",
|
||||
"Options.LogFilePath.Default": "لا يوجد مسار مخصص — تُحفظ السجلات في user/logs بجوار المحاكي.",
|
||||
"Options.LogFilePath.Select": "تحديد...",
|
||||
|
||||
"Options.OverrideLogFile.Label": "الكتابة فوق ملف السجل",
|
||||
"Options.OverrideLogFile.Desc": "استخدام مسار الملف الدقيق بدلاً من إلحاق معرف العنوان والطابع الزمني.",
|
||||
|
||||
"Options.TitleMusic.Label": "موسيقى اللعبة",
|
||||
"Options.TitleMusic.Desc": "تكرار موسيقى المعاينة للعبة المحددة في المكتبة.",
|
||||
|
||||
"Options.Discord.Label": "حالة دسكورد",
|
||||
"Options.Discord.Desc": "إظهار اللعبة قيد التشغيل في ملفك الشخصي على دسكورد.",
|
||||
|
||||
"Options.Language.Label": "لغة المحاكي",
|
||||
"Options.Language.Desc": "اللغة المستخدمة في جميع أنحاء المشغل. تُطبق فوراً.",
|
||||
|
||||
"Common.On": "تشغيل",
|
||||
"Common.Off": "إيقاف",
|
||||
|
||||
"Console.Title": "نافذة السجلات",
|
||||
"Console.SearchWatermark": "بحث...",
|
||||
"Console.AutoScroll": "تمرير تلقائي",
|
||||
"Console.Split": "تقسيم",
|
||||
"Console.Copy": "نسخ",
|
||||
"Console.Clear": "مسح",
|
||||
"Console.WindowTitle": "نافذة سجلات SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "لم تُحدد أي لعبة",
|
||||
"Launch.NoGameHint": "اختر لعبة من المكتبة، أو افتح ملف eboot.bin مباشرة.",
|
||||
"Launch.Idle": "خامل",
|
||||
"Launch.Console": "≡ نافذة السجلات",
|
||||
"Launch.Launch": "▶ تشغيل",
|
||||
"Launch.Stop": "■ إيقاف",
|
||||
"Launch.Running": "قيد التشغيل — {0}",
|
||||
"Launch.Stopping": "جارٍ الإيقاف...",
|
||||
"Launch.Exited": "انتهى برمز {0} ({1})",
|
||||
"Launch.ExeNotFound": "لم يُعثر على الملف التنفيذي SharpEmu. ابنِ مشروع SharpEmu.CLI أولاً (dotnet build).",
|
||||
"Launch.LogFile": "ملف السجل: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "فشل بدء تشغيل المحاكي: {0}",
|
||||
"Launch.ProcessExited": "انتهت العملية برمز {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "موافق",
|
||||
"Exit.InvalidArguments": "معطيات غير صالحة",
|
||||
"Exit.EbootNotFound": "لم يُعثر على eboot",
|
||||
"Exit.RuntimeException": "استثناء وقت التشغيل",
|
||||
"Exit.EmulationError": "خطأ في المحاكاة",
|
||||
"Exit.Unknown": "غير معروف",
|
||||
|
||||
"Status.EmulatorLocating": "المحاكي: جارٍ تحديد الموقع...",
|
||||
"Status.EmulatorPath": "المحاكي: {0}",
|
||||
"Status.EmulatorNotFound": "المحاكي: لم يُعثر على الملف التنفيذي SharpEmu — ابنِ SharpEmu.CLI أولاً.",
|
||||
"Status.ScanningLibrary": "جارٍ فحص المكتبة...",
|
||||
"Status.AddFolderPrompt": "أضف مجلد ألعاب لملء المكتبة.",
|
||||
"Status.LibraryScanned": "فُحصت المكتبة: {0} لعبة في {1} مجلد.",
|
||||
"Status.CouldNotOpenFolder": "تعذر فتح المجلد: {0}",
|
||||
"Status.CopiedToClipboard": "نُسخ {0} إلى الحافظة.",
|
||||
"Status.RemovedFromLibrary": "أُزيل “{0}” من المكتبة. أعد إضافة مجلده لاستعادته.",
|
||||
"Status.Running": "جارٍ تشغيل {0}",
|
||||
"Status.Stopping": "جارٍ الإيقاف...",
|
||||
"Status.Idle": "خامل",
|
||||
|
||||
"Clipboard.Path": "المسار",
|
||||
"Clipboard.TitleId": "معرف العنوان",
|
||||
|
||||
"Discord.Playing": "يلعب {0}",
|
||||
"Discord.Browsing": "يتصفح المكتبة",
|
||||
|
||||
"Dialog.ChooseGameFolder": "اختر مجلداً يحتوي على ألعاب",
|
||||
"Dialog.OpenExecutable": "افتح ملفاً تنفيذياً لتشغيله",
|
||||
"Dialog.PsExecutables": "ملفات PS التنفيذية",
|
||||
"Dialog.SaveLogFile": "حدد مكان حفظ ملف السجل",
|
||||
"Dialog.PlainTextFiles": "ملفات نصية عادية",
|
||||
"Dialog.LogFiles": "ملفات السجل"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Português (Brasil)",
|
||||
|
||||
"Page.Library": "Biblioteca",
|
||||
"Page.Options": "Opções",
|
||||
"Page.GameCount.One": "1 Jogo",
|
||||
"Page.GameCount.Other": "{0} jogos",
|
||||
|
||||
"Library.SearchWatermark": "Pesquisar na biblioteca…",
|
||||
"Library.AddFolder": "+ Adicionar pasta",
|
||||
"Library.Rescan": "⟳ Atualizar biblioteca",
|
||||
"Library.OpenFile": "Abrir arquivo…",
|
||||
|
||||
"Library.Context.Launch": "Jogar",
|
||||
"Library.Context.OpenFolder": "Abrir pasta do jogo",
|
||||
"Library.Context.CopyPath": "Copiar o caminho",
|
||||
"Library.Context.CopyTitleId": "Copiar ID do título",
|
||||
"Library.Context.Remove": "Remover da biblioteca",
|
||||
|
||||
"Library.Empty.Title": "Sua biblioteca está vazia",
|
||||
"Library.Empty.Hint": "Adicione uma pasta contendo seus jogos para começar.",
|
||||
"Library.Empty.SearchTitle": "Nenhum jogo corresponde à sua busca",
|
||||
"Library.Empty.SearchHint": "Nada na biblioteca corresponde a “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Adicionar pasta do jogo",
|
||||
|
||||
"Library.Loading": "Carregando biblioteca…",
|
||||
|
||||
"Options.General": "Opções Gerais",
|
||||
"Options.Section.Emulation": "EMULAÇÃO",
|
||||
"Options.Section.Logging": "LOGS",
|
||||
"Options.Section.Launcher": "INICIALIZADOR",
|
||||
|
||||
"Options.CpuEngine.Label": "Motor da CPU",
|
||||
"Options.CpuEngine.Desc": "Motor de execução usado para executar o código do jogo.",
|
||||
"Options.CpuEngine.Native": "Nativo",
|
||||
|
||||
"Options.Strict.Label": "Resolução estrita de bibliotecas dinâmicas",
|
||||
"Options.Strict.Desc": "Interrompe a inicialização caso um símbolo importado não possa ser vinculado.",
|
||||
|
||||
"Options.LogLevel.Label": "Nível de log",
|
||||
"Options.LogLevel.Desc": "Nível de detalhamento das mensagens exibidas no console do emulador.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warning",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Critical",
|
||||
|
||||
"Options.TraceImports.Label": "Limite de rastreamento de importações",
|
||||
"Options.TraceImports.Desc": "Rastreia as primeiras N importações de cada módulo (0 = desativado).",
|
||||
|
||||
"Options.LogToFile.Label": "Salvar log em arquivo",
|
||||
"Options.LogToFile.Desc": "Copia a saída do emulador para um arquivo de log.",
|
||||
|
||||
"Options.LogFilePath.Label": "Caminho do arquivo de log",
|
||||
"Options.LogFilePath.Default": "Nenhum caminho definido — logs vão para user/logs na pasta do emulador.",
|
||||
"Options.LogFilePath.Select": "Selecionar…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Sobrescrever arquivo de log",
|
||||
"Options.OverrideLogFile.Desc": "Use o caminho exato do arquivo em vez de adicionar o ID do título e o log de data e hora.",
|
||||
|
||||
"Options.TitleMusic.Label": "Música de prévia",
|
||||
"Options.TitleMusic.Desc": "Reproduz em loop a música de prévia do jogo selecionado na biblioteca.",
|
||||
|
||||
"Options.Discord.Label": "Status do Discord",
|
||||
"Options.Discord.Desc": "Exibir o jogo em execução no seu perfil do Discord.",
|
||||
|
||||
"Options.Language.Label": "Idioma do emulador",
|
||||
"Options.Language.Desc": "Idioma usado em toda a interface do emulador. A alteração é aplicada imediatamente.",
|
||||
|
||||
"Common.On": "Ativado",
|
||||
"Common.Off": "Desativado",
|
||||
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Pesquisar...",
|
||||
"Console.AutoScroll": "Rolagem automática",
|
||||
"Console.Split": "Recortar",
|
||||
"Console.Copy": "Copiar",
|
||||
"Console.Clear": "Limpar",
|
||||
"Console.WindowTitle": "Console do SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "Nenhum jogo selecionado",
|
||||
"Launch.NoGameHint": "Selecione um jogo na biblioteca ou abra um arquivo eboot.bin diretamente.",
|
||||
"Launch.Idle": "Ocioso",
|
||||
"Launch.Console": "≡ Console",
|
||||
"Launch.Launch": "▶ Iniciar",
|
||||
"Launch.Stop": "■ Parar",
|
||||
"Launch.Running": "Em execução — {0}",
|
||||
"Launch.Stopping": "Encerrando…",
|
||||
"Launch.Exited": "Encerrado com código {0} ({1})",
|
||||
"Launch.ExeNotFound": "Executável do SharpEmu não encontrado. Compile primeiro o projeto SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "Arquivo de log: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Falha ao iniciar o emulador: {0}",
|
||||
"Launch.ProcessExited": "O processo foi encerrado com código {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "argumentos inválidos",
|
||||
"Exit.EbootNotFound": "eboot.bin não encontrado",
|
||||
"Exit.RuntimeException": "exceção em tempo de execução",
|
||||
"Exit.EmulationError": "erro de emulação",
|
||||
"Exit.Unknown": "desconhecido",
|
||||
|
||||
"Status.EmulatorLocating": "Emulador: localizando…",
|
||||
"Status.EmulatorPath": "Emulador: {0}",
|
||||
"Status.EmulatorNotFound": "Emulador: executável do SharpEmu não encontrado — compile o SharpEmu.CLI primeiro.",
|
||||
"Status.ScanningLibrary": "Verificando biblioteca…",
|
||||
"Status.AddFolderPrompt": "Adicione uma pasta de jogos para preencher a biblioteca.",
|
||||
"Status.LibraryScanned": "Biblioteca verificada: {0} jogo(s) em {1} pasta(s).",
|
||||
"Status.CouldNotOpenFolder": "Não foi possível abrir a pasta: {0}",
|
||||
"Status.CopiedToClipboard": "{0} copiado para a área de transferência.",
|
||||
"Status.RemovedFromLibrary": "“{0}” removido da biblioteca. Adicione novamente sua pasta para restaurá-lo.",
|
||||
"Status.Running": "Executando {0}",
|
||||
"Status.Stopping": "Encerrando…",
|
||||
"Status.Idle": "Ocioso",
|
||||
|
||||
"Clipboard.Path": "Caminho",
|
||||
"Clipboard.TitleId": "ID do título",
|
||||
|
||||
"Discord.Playing": "Jogando {0}",
|
||||
"Discord.Browsing": "Navegando pela biblioteca",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Escolha uma pasta contendo jogos",
|
||||
"Dialog.OpenExecutable": "Abrir um executável para iniciar",
|
||||
"Dialog.PsExecutables": "Executáveis de PS",
|
||||
"Dialog.SaveLogFile": "Selecione onde salvar o arquivo de log",
|
||||
"Dialog.PlainTextFiles": "Arquivos de texto simples",
|
||||
"Dialog.LogFiles": "Arquivos de log"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Deutsch",
|
||||
|
||||
"Page.Library": "Bibliothek",
|
||||
"Page.Options": "Optionen",
|
||||
"Page.GameCount.One": "1 Spiel",
|
||||
"Page.GameCount.Other": "{0} Spiele",
|
||||
|
||||
"Library.SearchWatermark": "Bibliothek durchsuchen…",
|
||||
"Library.AddFolder": "+ Spielordner hinzufügen",
|
||||
"Library.Rescan": "⟳ Neu scannen",
|
||||
"Library.OpenFile": "Datei öffnen…",
|
||||
|
||||
"Library.Context.Launch": "Starten",
|
||||
"Library.Context.OpenFolder": "Spielordner öffnen",
|
||||
"Library.Context.CopyPath": "Pfad kopieren",
|
||||
"Library.Context.CopyTitleId": "Title ID kopieren",
|
||||
"Library.Context.Remove": "Aus Bibliothek entfernen",
|
||||
|
||||
"Library.Empty.Title": "Deine Bibliothek ist leer",
|
||||
"Library.Empty.Hint": "Füge einen Ordner mit deinen Spielen hinzu, um zu beginnen.",
|
||||
"Library.Empty.SearchTitle": "Keine Spiele gefunden",
|
||||
"Library.Empty.SearchHint": "Nichts in der Bibliothek entspricht "{0}".",
|
||||
"Library.Empty.AddFolder": "+ Spielordner hinzufügen",
|
||||
|
||||
"Library.Loading": "Bibliothek wird geladen…",
|
||||
|
||||
"Options.General": "Allgemein",
|
||||
"Options.Section.Emulation": "EMULATION",
|
||||
"Options.Section.Logging": "PROTOKOLLIERUNG",
|
||||
"Options.Section.Launcher": "LAUNCHER",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU-Engine",
|
||||
"Options.CpuEngine.Desc": "Ausführungs-Engine, die zum Ausführen des Spiel-Codes verwendet wird.",
|
||||
"Options.CpuEngine.Native": "Nativ",
|
||||
|
||||
"Options.Strict.Label": "Strikte dynlib-Auflösung",
|
||||
"Options.Strict.Desc": "Starten abbrechen, wenn ein importiertes Symbol nicht aufgelöst werden kann.",
|
||||
|
||||
"Options.LogLevel.Label": "Protokollstufe",
|
||||
"Options.LogLevel.Desc": "Ausführlichkeit der Emulator-Konsolenausgabe.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warnung",
|
||||
"Options.LogLevel.Error": "Fehler",
|
||||
"Options.LogLevel.Critical": "Kritisch",
|
||||
|
||||
"Options.TraceImports.Label": "Import-Trace-Limit",
|
||||
"Options.TraceImports.Desc": "Die ersten N Imports pro Modul verfolgen (0 = aus).",
|
||||
|
||||
"Options.LogToFile.Label": "In Datei protokollieren",
|
||||
"Options.LogToFile.Desc": "Emulator-Ausgabe zusätzlich in eine Log-Datei schreiben.",
|
||||
|
||||
"Options.LogFilePath.Label": "Protokolldatei-Pfad",
|
||||
"Options.LogFilePath.Default": "Kein benutzerdefinierter Pfad – Logs werden im Ordner user/logs neben dem Emulator gespeichert.",
|
||||
"Options.LogFilePath.Select": "Auswählen…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Protokolldatei überschreiben",
|
||||
"Options.OverrideLogFile.Desc": "Genauen Dateipfad verwenden, statt Title-ID und Zeitstempel anzuhängen.",
|
||||
|
||||
"Options.TitleMusic.Label": "Titel-Musik",
|
||||
"Options.TitleMusic.Desc": "Die Vorschau-Musik des ausgewählten Spiels in der Bibliothek loopend abspielen.",
|
||||
|
||||
"Options.Discord.Label": "Discord-Präsenz",
|
||||
"Options.Discord.Desc": "Zeigt das aktuell gespielte Spiel in deinem Discord-Profil an.",
|
||||
|
||||
"Options.Language.Label": "Emulator-Sprache",
|
||||
"Options.Language.Desc": "Sprache der Benutzeroberfläche. Wird sofort angewendet.",
|
||||
|
||||
"Common.On": "An",
|
||||
"Common.Off": "Aus",
|
||||
|
||||
"Console.Title": "KONSOLE",
|
||||
"Console.SearchWatermark": "Suchen...",
|
||||
"Console.AutoScroll": "Auto-Scroll",
|
||||
"Console.Split": "Teilen",
|
||||
"Console.Copy": "Kopieren",
|
||||
"Console.Clear": "Leeren",
|
||||
"Console.WindowTitle": "SharpEmu Konsole",
|
||||
|
||||
"Launch.NoGameSelected": "Kein Spiel ausgewählt",
|
||||
"Launch.NoGameHint": "Wähle ein Spiel aus der Bibliothek aus oder öffne eine eboot.bin direkt.",
|
||||
"Launch.Idle": "Bereit",
|
||||
"Launch.Console": "≡ Konsole",
|
||||
"Launch.Launch": "▶ Starten",
|
||||
"Launch.Stop": "■ Stoppen",
|
||||
"Launch.Running": "Läuft -- {0}",
|
||||
"Launch.Stopping": "Wird beendet…",
|
||||
"Launch.Exited": "Beendet mit Code {0} ({1})",
|
||||
"Launch.ExeNotFound": "SharpEmu-Executable wurde nicht gefunden. Baue zuerst das SharpEmu.CLI-Projekt (dotnet build).",
|
||||
"Launch.LogFile": "Log-Datei: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Emulator konnte nicht gestartet werden: {0}",
|
||||
"Launch.ProcessExited": "Prozess wurde mit Code {0} ({1}) beendet.",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "Ungültige Argumente",
|
||||
"Exit.EbootNotFound": "eboot nicht gefunden",
|
||||
"Exit.RuntimeException": "Laufzeitfehler",
|
||||
"Exit.EmulationError": "Emulationsfehler",
|
||||
"Exit.Unknown": "unbekannt",
|
||||
|
||||
"Status.EmulatorLocating": "Emulator: wird gesucht…",
|
||||
"Status.EmulatorPath": "Emulator: {0}",
|
||||
"Status.EmulatorNotFound": "Emulator: SharpEmu-Executable nicht gefunden -- baue zuerst SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Bibliothek wird gescannt…",
|
||||
"Status.AddFolderPrompt": "Füge einen Spielordner hinzu, um die Bibliothek zu füllen.",
|
||||
"Status.LibraryScanned": "Bibliothek gescannt: {0} Spiel(e) in {1} Ordner(n).",
|
||||
"Status.CouldNotOpenFolder": "Ordner konnte nicht geöffnet werden: {0}",
|
||||
"Status.CopiedToClipboard": "{0} in die Zwischenablage kopiert.",
|
||||
"Status.RemovedFromLibrary": ""{0}" wurde aus der Bibliothek entfernt. Füge den Ordner erneut hinzu, um es wiederherzustellen.",
|
||||
"Status.Running": "Läuft {0}",
|
||||
"Status.Stopping": "Wird gestoppt…",
|
||||
"Status.Idle": "Bereit",
|
||||
|
||||
"Clipboard.Path": "Pfad",
|
||||
"Clipboard.TitleId": "Title ID",
|
||||
|
||||
"Discord.Playing": "Spielt {0}",
|
||||
"Discord.Browsing": "Durchsucht die Bibliothek",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Spielordner auswählen",
|
||||
"Dialog.OpenExecutable": "Ausführbare Datei zum Starten öffnen",
|
||||
"Dialog.PsExecutables": "PS-Ausführbare Dateien",
|
||||
"Dialog.SaveLogFile": "Protokolldatei speichern unter",
|
||||
"Dialog.PlainTextFiles": "Textdateien",
|
||||
"Dialog.LogFiles": "Protokolldateien"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Dansk",
|
||||
|
||||
"Page.Library": "Bibliotek",
|
||||
"Page.Options": "Indstillinger",
|
||||
"Page.GameCount.One": "1 spil",
|
||||
"Page.GameCount.Other": "{0} spil",
|
||||
|
||||
"Library.SearchWatermark": "Søg i biblioteket…",
|
||||
"Library.AddFolder": "+ Tilføj mappe",
|
||||
"Library.Rescan": "⟳ Genindlæs",
|
||||
"Library.OpenFile": "Åbn fil…",
|
||||
|
||||
"Library.Context.Launch": "Start",
|
||||
"Library.Context.OpenFolder": "Åbn spilmappe",
|
||||
"Library.Context.CopyPath": "Kopiér sti",
|
||||
"Library.Context.CopyTitleId": "Kopiér titel-ID",
|
||||
"Library.Context.Remove": "Fjern fra bibliotek",
|
||||
|
||||
"Library.Empty.Title": "Dit bibliotek er tomt",
|
||||
"Library.Empty.Hint": "Tilføj en mappe med dine spil for at komme i gang.",
|
||||
"Library.Empty.SearchTitle": "Ingen spil matcher din søgning",
|
||||
"Library.Empty.SearchHint": "Intet i biblioteket matcher “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Tilføj spilmappe",
|
||||
|
||||
"Library.Loading": "Indlæser bibliotek…",
|
||||
|
||||
"Options.General": "Generelt",
|
||||
"Options.Section.Emulation": "EMULERING",
|
||||
"Options.Section.Logging": "LOGGING",
|
||||
"Options.Section.Launcher": "LAUNCHER",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU-engine",
|
||||
"Options.CpuEngine.Desc": "Den eksekveringsengine der bruges til at køre spilkode.",
|
||||
"Options.CpuEngine.Native": "Native",
|
||||
|
||||
"Options.Strict.Label": "Streng dynlib-opløsning",
|
||||
"Options.Strict.Desc": "Afbryd opstarten, når et importeret symbol ikke kan findes.",
|
||||
|
||||
"Options.LogLevel.Label": "Logniveau",
|
||||
"Options.LogLevel.Desc": "Detaljeringsgrad for emulatorens konsoloutput.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Advarsel",
|
||||
"Options.LogLevel.Error": "Fejl",
|
||||
"Options.LogLevel.Critical": "Kritisk",
|
||||
|
||||
"Options.TraceImports.Label": "Grænse for import-trace",
|
||||
"Options.TraceImports.Desc": "Spor de første N imports pr. modul (0 = fra).",
|
||||
|
||||
"Options.LogToFile.Label": "Log til fil",
|
||||
"Options.LogToFile.Desc": "Spejl emulatorens output til en logfil.",
|
||||
|
||||
"Options.LogFilePath.Label": "Sti til logfil",
|
||||
"Options.LogFilePath.Default": "Ingen brugerdefineret sti — logs gemmes i user/logs ved siden af emulatoren.",
|
||||
"Options.LogFilePath.Select": "Vælg…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Tilsidesæt logfil",
|
||||
"Options.OverrideLogFile.Desc": "Brug den præcise filsti i stedet for at tilføje titel-ID og tidsstempel.",
|
||||
|
||||
"Options.TitleMusic.Label": "Titelmusik",
|
||||
"Options.TitleMusic.Desc": "Gentag det valgte spils forhåndsvisningsmusik i biblioteket.",
|
||||
|
||||
"Options.Discord.Label": "Discord-tilstedeværelse",
|
||||
"Options.Discord.Desc": "Vis det kørende spil på din Discord-profil.",
|
||||
|
||||
"Options.Language.Label": "Emulatorsprog",
|
||||
"Options.Language.Desc": "Sprog der bruges i hele launcheren. Anvendes med det samme.",
|
||||
|
||||
"Common.On": "Til",
|
||||
"Common.Off": "Fra",
|
||||
|
||||
"Console.Title": "KONSOL",
|
||||
"Console.SearchWatermark": "Søg...",
|
||||
"Console.AutoScroll": "Auto-scroll",
|
||||
"Console.Split": "Opdel",
|
||||
"Console.Copy": "Kopiér",
|
||||
"Console.Clear": "Ryd",
|
||||
"Console.WindowTitle": "SharpEmu-konsol",
|
||||
|
||||
"Launch.NoGameSelected": "Intet spil valgt",
|
||||
"Launch.NoGameHint": "Vælg et spil fra biblioteket, eller åbn en eboot.bin direkte.",
|
||||
"Launch.Idle": "Inaktiv",
|
||||
"Launch.Console": "≡ Konsol",
|
||||
"Launch.Launch": "▶ Start",
|
||||
"Launch.Stop": "■ Stop",
|
||||
"Launch.Running": "Kører — {0}",
|
||||
"Launch.Stopping": "Stopper…",
|
||||
"Launch.Exited": "Afsluttet med kode {0} ({1})",
|
||||
"Launch.ExeNotFound": "SharpEmu-programmet blev ikke fundet. Byg SharpEmu.CLI-projektet først (dotnet build).",
|
||||
"Launch.LogFile": "Logfil: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Kunne ikke starte emulatoren: {0}",
|
||||
"Launch.ProcessExited": "Processen afsluttede med kode {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "ugyldige argumenter",
|
||||
"Exit.EbootNotFound": "eboot ikke fundet",
|
||||
"Exit.RuntimeException": "runtime-fejl",
|
||||
"Exit.EmulationError": "emuleringsfejl",
|
||||
"Exit.Unknown": "ukendt",
|
||||
|
||||
"Status.EmulatorLocating": "Emulator: lokaliserer…",
|
||||
"Status.EmulatorPath": "Emulator: {0}",
|
||||
"Status.EmulatorNotFound": "Emulator: SharpEmu-programmet blev ikke fundet — byg SharpEmu.CLI først.",
|
||||
"Status.ScanningLibrary": "Skanner bibliotek…",
|
||||
"Status.AddFolderPrompt": "Tilføj en spilmappe for at udfylde biblioteket.",
|
||||
"Status.LibraryScanned": "Bibliotek skannet: {0} spil i {1} mappe(r).",
|
||||
"Status.CouldNotOpenFolder": "Kunne ikke åbne mappe: {0}",
|
||||
"Status.CopiedToClipboard": "{0} kopieret til udklipsholderen.",
|
||||
"Status.RemovedFromLibrary": "“{0}” fjernet fra biblioteket. Tilføj mappen igen for at gendanne det.",
|
||||
"Status.Running": "Kører {0}",
|
||||
"Status.Stopping": "Stopper…",
|
||||
"Status.Idle": "Inaktiv",
|
||||
|
||||
"Clipboard.Path": "Sti",
|
||||
"Clipboard.TitleId": "Titel-ID",
|
||||
|
||||
"Discord.Playing": "Spiller {0}",
|
||||
"Discord.Browsing": "Gennemser biblioteket",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Vælg en mappe der indeholder spil",
|
||||
"Dialog.OpenExecutable": "Åbn et program der skal startes",
|
||||
"Dialog.PsExecutables": "PS-programmer",
|
||||
"Dialog.SaveLogFile": "Vælg hvor logfilen skal gemmes",
|
||||
"Dialog.PlainTextFiles": "Almindelige tekstfiler",
|
||||
"Dialog.LogFiles": "Logfiler"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "English",
|
||||
|
||||
"Page.Library": "Library",
|
||||
"Page.Options": "Options",
|
||||
"Page.GameCount.One": "1 game",
|
||||
"Page.GameCount.Other": "{0} games",
|
||||
|
||||
"Library.SearchWatermark": "Search library…",
|
||||
"Library.AddFolder": "+ Add folder",
|
||||
"Library.Rescan": "⟳ Rescan",
|
||||
"Library.OpenFile": "Open file…",
|
||||
|
||||
"Library.Context.Launch": "Launch",
|
||||
"Library.Context.OpenFolder": "Open game folder",
|
||||
"Library.Context.CopyPath": "Copy path",
|
||||
"Library.Context.CopyTitleId": "Copy title ID",
|
||||
"Library.Context.Remove": "Remove from library",
|
||||
|
||||
"Library.Empty.Title": "Your library is empty",
|
||||
"Library.Empty.Hint": "Add a folder containing your games to get started.",
|
||||
"Library.Empty.SearchTitle": "No games match your search",
|
||||
"Library.Empty.SearchHint": "Nothing in the library matches “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Add game folder",
|
||||
|
||||
"Library.Loading": "Loading library…",
|
||||
|
||||
"Options.General": "General",
|
||||
"Options.Section.Emulation": "EMULATION",
|
||||
"Options.Section.Logging": "LOGGING",
|
||||
"Options.Section.Launcher": "LAUNCHER",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU engine",
|
||||
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
|
||||
"Options.CpuEngine.Native": "Native",
|
||||
|
||||
"Options.Strict.Label": "Strict dynlib resolution",
|
||||
"Options.Strict.Desc": "Fail the launch when an imported symbol cannot be resolved.",
|
||||
|
||||
"Options.LogLevel.Label": "Log level",
|
||||
"Options.LogLevel.Desc": "Verbosity of the emulator console output.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warning",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Critical",
|
||||
|
||||
"Options.TraceImports.Label": "Import trace limit",
|
||||
"Options.TraceImports.Desc": "Trace the first N imports per module (0 = off).",
|
||||
|
||||
"Options.LogToFile.Label": "Log to file",
|
||||
"Options.LogToFile.Desc": "Mirror emulator output to a log file.",
|
||||
|
||||
"Options.LogFilePath.Label": "Log file path",
|
||||
"Options.LogFilePath.Default": "No custom path — logs go to user/logs next to the emulator.",
|
||||
"Options.LogFilePath.Select": "Select…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Override log file",
|
||||
"Options.OverrideLogFile.Desc": "Use the exact file path instead of appending title ID and timestamp.",
|
||||
|
||||
"Options.TitleMusic.Label": "Title music",
|
||||
"Options.TitleMusic.Desc": "Loop the selected game's preview music in the library.",
|
||||
|
||||
"Options.Discord.Label": "Discord presence",
|
||||
"Options.Discord.Desc": "Show the running game on your Discord profile.",
|
||||
|
||||
"Options.Language.Label": "Emulator language",
|
||||
"Options.Language.Desc": "Language used throughout the launcher. Applies immediately.",
|
||||
|
||||
"Common.On": "On",
|
||||
"Common.Off": "Off",
|
||||
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Search...",
|
||||
"Console.AutoScroll": "Auto-scroll",
|
||||
"Console.Split": "Split",
|
||||
"Console.Copy": "Copy",
|
||||
"Console.Clear": "Clear",
|
||||
"Console.WindowTitle": "SharpEmu Console",
|
||||
|
||||
"Launch.NoGameSelected": "No game selected",
|
||||
"Launch.NoGameHint": "Pick a game from the library, or open an eboot.bin directly.",
|
||||
"Launch.Idle": "Idle",
|
||||
"Launch.Console": "≡ Console",
|
||||
"Launch.Launch": "▶ Launch",
|
||||
"Launch.Stop": "■ Stop",
|
||||
"Launch.Running": "Running — {0}",
|
||||
"Launch.Stopping": "Stopping…",
|
||||
"Launch.Exited": "Exited with code {0} ({1})",
|
||||
"Launch.ExeNotFound": "SharpEmu executable not found. Build the SharpEmu.CLI project first (dotnet build).",
|
||||
"Launch.LogFile": "Log file: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Failed to start the emulator: {0}",
|
||||
"Launch.ProcessExited": "Process exited with code {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "invalid arguments",
|
||||
"Exit.EbootNotFound": "eboot not found",
|
||||
"Exit.RuntimeException": "runtime exception",
|
||||
"Exit.EmulationError": "emulation error",
|
||||
"Exit.Unknown": "unknown",
|
||||
|
||||
"Status.EmulatorLocating": "Emulator: locating…",
|
||||
"Status.EmulatorPath": "Emulator: {0}",
|
||||
"Status.EmulatorNotFound": "Emulator: SharpEmu executable not found — build SharpEmu.CLI first.",
|
||||
"Status.ScanningLibrary": "Scanning library…",
|
||||
"Status.AddFolderPrompt": "Add a game folder to populate the library.",
|
||||
"Status.LibraryScanned": "Library scanned: {0} game(s) in {1} folder(s).",
|
||||
"Status.CouldNotOpenFolder": "Could not open folder: {0}",
|
||||
"Status.CopiedToClipboard": "{0} copied to clipboard.",
|
||||
"Status.RemovedFromLibrary": "Removed “{0}” from the library. Re-add its folder to restore it.",
|
||||
"Status.Running": "Running {0}",
|
||||
"Status.Stopping": "Stopping…",
|
||||
"Status.Idle": "Idle",
|
||||
|
||||
"Clipboard.Path": "Path",
|
||||
"Clipboard.TitleId": "Title ID",
|
||||
|
||||
"Discord.Playing": "Playing {0}",
|
||||
"Discord.Browsing": "Browsing the library",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Choose a folder containing games",
|
||||
"Dialog.OpenExecutable": "Open an executable to launch",
|
||||
"Dialog.PsExecutables": "PS executables",
|
||||
"Dialog.SaveLogFile": "Select where to save the Log file",
|
||||
"Dialog.PlainTextFiles": "Plain Text Files",
|
||||
"Dialog.LogFiles": "Log Files"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Español",
|
||||
|
||||
"Page.Library": "Biblioteca",
|
||||
"Page.Options": "Opciones",
|
||||
"Page.GameCount.One": "1 juego",
|
||||
"Page.GameCount.Other": "{0} juegos",
|
||||
|
||||
"Library.SearchWatermark": "Buscar en la biblioteca…",
|
||||
"Library.AddFolder": "+ Añadir carpeta",
|
||||
"Library.Rescan": "⟳ Volver a escanear",
|
||||
"Library.OpenFile": "Abrir archivo…",
|
||||
|
||||
"Library.Context.Launch": "Iniciar",
|
||||
"Library.Context.OpenFolder": "Abrir carpeta de juegos",
|
||||
"Library.Context.CopyPath": "Copiar ruta",
|
||||
"Library.Context.CopyTitleId": "Copiar ID del título",
|
||||
"Library.Context.Remove": "Eliminar de la biblioteca",
|
||||
|
||||
"Library.Empty.Title": "Tu biblioteca está vacía",
|
||||
"Library.Empty.Hint": "Añade una carpeta que contenga tus juegos para empezar.",
|
||||
"Library.Empty.SearchTitle": "Ningún juego coincide con la búsqueda",
|
||||
"Library.Empty.SearchHint": "No se ha encontrado nada en la biblioteca que coincida con “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Añadir carpeta de juegos",
|
||||
|
||||
"Library.Loading": "Cargando biblioteca…",
|
||||
|
||||
"Options.General": "General",
|
||||
"Options.Section.Emulation": "EMULACIÓN",
|
||||
"Options.Section.Logging": "LOGS",
|
||||
"Options.Section.Launcher": "LAUNCHER",
|
||||
|
||||
"Options.CpuEngine.Label": "Motor de CPU",
|
||||
"Options.CpuEngine.Desc": "Motor utilizado para ejecutar el código del juego.",
|
||||
"Options.CpuEngine.Native": "Nativo",
|
||||
|
||||
"Options.Strict.Label": "Resolución estricta de dynlib (Bibliotecas dinámicas)",
|
||||
"Options.Strict.Desc": "Detener la ejecución cuando un símbolo importado no se pueda resolver.",
|
||||
|
||||
"Options.LogLevel.Label": "Nivel de Log",
|
||||
"Options.LogLevel.Desc": "Verbosidad de la salida en consola del emulador.",
|
||||
"Options.LogLevel.Trace": "Trazas",
|
||||
"Options.LogLevel.Debug": "Depuración",
|
||||
"Options.LogLevel.Info": "Información",
|
||||
"Options.LogLevel.Warning": "Advertencia",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Crítico",
|
||||
|
||||
"Options.TraceImports.Label": "Límite de trazado de importaciones",
|
||||
"Options.TraceImports.Desc": "Trazar las primeras N importaciones por módulo (0 = off).",
|
||||
|
||||
"Options.LogToFile.Label": "Registrar log en archivo",
|
||||
"Options.LogToFile.Desc": "Duplicar la salida del emulador en un archivo de logs.",
|
||||
|
||||
"Options.LogFilePath.Label": "Ruta del archivo de Log",
|
||||
"Options.LogFilePath.Default": "Sin ruta personalizada — los logs van a user/logs al lado del emulador.",
|
||||
"Options.LogFilePath.Select": "Seleccionar…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Sobreescribir archivo de logs",
|
||||
"Options.OverrideLogFile.Desc": "Utilizar la misma ruta para el archivo de logs en vez de añadir la ID del título y marca de tiempo.",
|
||||
|
||||
"Options.TitleMusic.Label": "Música del título",
|
||||
"Options.TitleMusic.Desc": "Repetir en bucle la preview de la música del juego seleccionado en la biblioteca.",
|
||||
|
||||
"Options.Discord.Label": "Actividad de Discord",
|
||||
"Options.Discord.Desc": "Mostrar juego en ejecución en tu perfil de Discord.",
|
||||
|
||||
"Options.Language.Label": "Idioma del emulador",
|
||||
"Options.Language.Desc": "Idioma utilizado en todo el launcher. Se aplica inmediatamente.",
|
||||
|
||||
"Common.On": "Encendido",
|
||||
"Common.Off": "Apagado",
|
||||
|
||||
"Console.Title": "CONSOLA",
|
||||
"Console.SearchWatermark": "Buscar...",
|
||||
"Console.AutoScroll": "Desplazamiento automático",
|
||||
"Console.Split": "Desacoplar",
|
||||
"Console.Copy": "Copiar",
|
||||
"Console.Clear": "Limpiar",
|
||||
"Console.WindowTitle": "Consola SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "No hay ningún juego seleccionado",
|
||||
"Launch.NoGameHint": "Selecciona un juego de la biblioteca o abre un eboot.bin directamente.",
|
||||
"Launch.Idle": "Inactivo",
|
||||
"Launch.Console": "≡ Consola",
|
||||
"Launch.Launch": "▶ Iniciar",
|
||||
"Launch.Stop": "■ Detener",
|
||||
"Launch.Running": "En ejecución — {0}",
|
||||
"Launch.Stopping": "Deteniendo…",
|
||||
"Launch.Exited": "Finalizó con el código {0} ({1})",
|
||||
"Launch.ExeNotFound": "No se ha encontrado el ejecutable de SharpEmu. Compila previamente el proyecto SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "Archivo de Log: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Error al iniciar el emulador: {0}",
|
||||
"Launch.ProcessExited": "El proceso finalizó con el código {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "argumentos no válidos",
|
||||
"Exit.EbootNotFound": "no se encontró eboot",
|
||||
"Exit.RuntimeException": "excepción en tiempo de ejecución",
|
||||
"Exit.EmulationError": "error de emulación",
|
||||
"Exit.Unknown": "desconocido",
|
||||
|
||||
"Status.EmulatorLocating": "Emulador: localizando…",
|
||||
"Status.EmulatorPath": "Emulador: {0}",
|
||||
"Status.EmulatorNotFound": "Emulador: No se encontró el ejecutable de SharpEmu — compila previamente SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Escaneando biblioteca…",
|
||||
"Status.AddFolderPrompt": "Añade una carpeta de juegos para poblar la biblioteca.",
|
||||
"Status.LibraryScanned": "Biblioteca escaneada: Se encontraron {0} juego(s) en {1} carpeta(s).",
|
||||
"Status.CouldNotOpenFolder": "No se ha podido abrir la carpeta: {0}",
|
||||
"Status.CopiedToClipboard": "{0} copiado al portapapeles.",
|
||||
"Status.RemovedFromLibrary": "Se eliminó “{0}” de la biblioteca. Vuelve a añadir su carpeta para restaurarlo.",
|
||||
"Status.Running": "Ejecutando {0}",
|
||||
"Status.Stopping": "Deteniendo…",
|
||||
"Status.Idle": "Inactivo",
|
||||
|
||||
"Clipboard.Path": "Ruta",
|
||||
"Clipboard.TitleId": "ID del título",
|
||||
|
||||
"Discord.Playing": "Jugando a {0}",
|
||||
"Discord.Browsing": "Navegando en la biblioteca, buscando un juego para divertirse.",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Selecciona una carpeta que contenga juegos",
|
||||
"Dialog.OpenExecutable": "Abrir un ejecutable para iniciar",
|
||||
"Dialog.PsExecutables": "Ejecutables de PS",
|
||||
"Dialog.SaveLogFile": "Selecciona dónde guardar el archivo de Logs",
|
||||
"Dialog.PlainTextFiles": "Archivos en texto plano",
|
||||
"Dialog.LogFiles": "Archivos de Log"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Français",
|
||||
|
||||
"Page.Library": "Bibliothèque",
|
||||
"Page.Options": "Options",
|
||||
"Page.GameCount.One": "1 jeu",
|
||||
"Page.GameCount.Other": "{0} jeux",
|
||||
|
||||
"Library.SearchWatermark": "Rechercher dans la bibliothèque…",
|
||||
"Library.AddFolder": "+ Ajouter un dossier",
|
||||
"Library.Rescan": "⟳ Analyser à nouveau",
|
||||
"Library.OpenFile": "Ouvrir un fichier…",
|
||||
|
||||
"Library.Context.Launch": "Lancer",
|
||||
"Library.Context.OpenFolder": "Ouvrir le dossier du jeu",
|
||||
"Library.Context.CopyPath": "Copier le chemin",
|
||||
"Library.Context.CopyTitleId": "Copier l’identifiant du jeu",
|
||||
"Library.Context.Remove": "Retirer de la bibliothèque",
|
||||
|
||||
"Library.Empty.Title": "Votre bibliothèque est vide",
|
||||
"Library.Empty.Hint": "Ajoutez un dossier contenant vos jeux pour commencer.",
|
||||
"Library.Empty.SearchTitle": "Aucun jeu ne correspond à votre recherche",
|
||||
"Library.Empty.SearchHint": "Aucun élément de la bibliothèque ne correspond à « {0} ».",
|
||||
"Library.Empty.AddFolder": "+ Ajouter un dossier de jeux",
|
||||
|
||||
"Library.Loading": "Chargement de la bibliothèque…",
|
||||
|
||||
"Options.General": "Général",
|
||||
"Options.Section.Emulation": "ÉMULATION",
|
||||
"Options.Section.Logging": "JOURNALISATION",
|
||||
"Options.Section.Launcher": "LANCEUR",
|
||||
|
||||
"Options.CpuEngine.Label": "Moteur CPU",
|
||||
"Options.CpuEngine.Desc": "Moteur d’exécution utilisé pour exécuter le code du jeu.",
|
||||
"Options.CpuEngine.Native": "Natif",
|
||||
|
||||
"Options.Strict.Label": "Résolution stricte des bibliothèques dynamiques",
|
||||
"Options.Strict.Desc": "Interrompre le lancement lorsqu’un symbole importé ne peut pas être résolu.",
|
||||
|
||||
"Options.LogLevel.Label": "Niveau de journalisation",
|
||||
"Options.LogLevel.Desc": "Niveau de détail des messages affichés dans la console de l’émulateur.",
|
||||
"Options.LogLevel.Trace": "Traçage",
|
||||
"Options.LogLevel.Debug": "Débogage",
|
||||
"Options.LogLevel.Info": "Informations",
|
||||
"Options.LogLevel.Warning": "Avertissements",
|
||||
"Options.LogLevel.Error": "Erreurs",
|
||||
"Options.LogLevel.Critical": "Erreurs critiques",
|
||||
|
||||
"Options.TraceImports.Label": "Limite de traçage des imports",
|
||||
"Options.TraceImports.Desc": "Tracer les N premiers imports de chaque module (0 = désactivé).",
|
||||
|
||||
"Options.LogToFile.Label": "Enregistrer dans un fichier",
|
||||
"Options.LogToFile.Desc": "Copier la sortie de l’émulateur dans un fichier journal.",
|
||||
|
||||
"Options.LogFilePath.Label": "Chemin du fichier journal",
|
||||
"Options.LogFilePath.Default": "Aucun chemin personnalisé — les journaux sont enregistrés dans user/logs à côté de l’émulateur.",
|
||||
"Options.LogFilePath.Select": "Sélectionner…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Remplacer le fichier journal",
|
||||
"Options.OverrideLogFile.Desc": "Utiliser exactement ce chemin au lieu d’ajouter l’identifiant du jeu et l’horodatage.",
|
||||
|
||||
"Options.TitleMusic.Label": "Musique du jeu",
|
||||
"Options.TitleMusic.Desc": "Lire en boucle la musique d’aperçu du jeu sélectionné dans la bibliothèque.",
|
||||
|
||||
"Options.Discord.Label": "Présence Discord",
|
||||
"Options.Discord.Desc": "Afficher le jeu en cours d’exécution sur votre profil Discord.",
|
||||
|
||||
"Options.Language.Label": "Langue de l’émulateur",
|
||||
"Options.Language.Desc": "Langue utilisée dans l’ensemble du lanceur. Le changement est immédiat.",
|
||||
|
||||
"Common.On": "Activé",
|
||||
"Common.Off": "Désactivé",
|
||||
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Rechercher…",
|
||||
"Console.AutoScroll": "Défilement automatique",
|
||||
"Console.Split": "Détacher",
|
||||
"Console.Copy": "Copier",
|
||||
"Console.Clear": "Effacer",
|
||||
"Console.WindowTitle": "Console SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "Aucun jeu sélectionné",
|
||||
"Launch.NoGameHint": "Choisissez un jeu dans la bibliothèque ou ouvrez directement un fichier eboot.bin.",
|
||||
"Launch.Idle": "Inactif",
|
||||
"Launch.Console": "≡ Console",
|
||||
"Launch.Launch": "▶ Lancer",
|
||||
"Launch.Stop": "■ Arrêter",
|
||||
"Launch.Running": "En cours d’exécution — {0}",
|
||||
"Launch.Stopping": "Arrêt en cours…",
|
||||
"Launch.Exited": "Processus terminé avec le code {0} ({1})",
|
||||
"Launch.ExeNotFound": "L’exécutable SharpEmu est introuvable. Compilez d’abord le projet SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "Fichier journal : {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Impossible de démarrer l’émulateur : {0}",
|
||||
"Launch.ProcessExited": "Le processus s’est terminé avec le code {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "arguments non valides",
|
||||
"Exit.EbootNotFound": "eboot introuvable",
|
||||
"Exit.RuntimeException": "exception d’exécution",
|
||||
"Exit.EmulationError": "erreur d’émulation",
|
||||
"Exit.Unknown": "inconnu",
|
||||
|
||||
"Status.EmulatorLocating": "Émulateur : recherche en cours…",
|
||||
"Status.EmulatorPath": "Émulateur : {0}",
|
||||
"Status.EmulatorNotFound": "Émulateur : l’exécutable SharpEmu est introuvable — compilez d’abord SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Analyse de la bibliothèque…",
|
||||
"Status.AddFolderPrompt": "Ajoutez un dossier de jeux pour remplir la bibliothèque.",
|
||||
"Status.LibraryScanned": "Bibliothèque analysée : {0} jeu(x) dans {1} dossier(s).",
|
||||
"Status.CouldNotOpenFolder": "Impossible d’ouvrir le dossier : {0}",
|
||||
"Status.CopiedToClipboard": "{0} copié dans le presse-papiers.",
|
||||
"Status.RemovedFromLibrary": "« {0} » a été retiré de la bibliothèque. Ajoutez à nouveau son dossier pour le restaurer.",
|
||||
"Status.Running": "Exécution de {0}",
|
||||
"Status.Stopping": "Arrêt en cours…",
|
||||
"Status.Idle": "Inactif",
|
||||
|
||||
"Clipboard.Path": "Chemin",
|
||||
"Clipboard.TitleId": "Identifiant du jeu",
|
||||
|
||||
"Discord.Playing": "Joue à {0}",
|
||||
"Discord.Browsing": "Parcourt la bibliothèque",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Choisir un dossier contenant des jeux",
|
||||
"Dialog.OpenExecutable": "Ouvrir un exécutable à lancer",
|
||||
"Dialog.PsExecutables": "Exécutables PlayStation",
|
||||
"Dialog.SaveLogFile": "Choisir l’emplacement du fichier journal",
|
||||
"Dialog.PlainTextFiles": "Fichiers texte brut",
|
||||
"Dialog.LogFiles": "Fichiers journaux"
|
||||
}
|
||||
@@ -0,0 +1,134 @@
|
||||
{
|
||||
"_languageName": "Italiano",
|
||||
|
||||
"Page.Library": "Libreria",
|
||||
"Page.Options": "Opzioni",
|
||||
"Page.GameCount.One": "1 gioco",
|
||||
"Page.GameCount.Other": "{0} giochi",
|
||||
|
||||
"Library.SearchWatermark": "Cerca nella libreria…",
|
||||
"Library.AddFolder": "+ Aggiungi cartella",
|
||||
"Library.Rescan": "⟳ Riscansiona",
|
||||
"Library.OpenFile": "Apri file…",
|
||||
|
||||
"Library.Context.Launch": "Avvia",
|
||||
"Library.Context.OpenFolder": "Apri cartella gioco",
|
||||
"Library.Context.CopyPath": "Copia percorso",
|
||||
"Library.Context.CopyTitleId": "Copia ID titolo",
|
||||
"Library.Context.Remove": "Rimuovi dalla libreria",
|
||||
|
||||
"Library.Empty.Title": "La tua libreria è vuota",
|
||||
"Library.Empty.Hint": "Aggiungi la cartella che contiene i tuoi giochi per partire.",
|
||||
"Library.Empty.SearchTitle": "Nessun gioco corrisponde alla ricerca",
|
||||
"Library.Empty.SearchHint": "Nulla nella libreria corrisponde a “{0}”.",
|
||||
"Library.Empty.AddFolder": "+ Aggiungi cartella giochi",
|
||||
|
||||
"Library.Loading": "Caricamento libreria…",
|
||||
|
||||
"Options.General": "Generale",
|
||||
"Options.Section.Emulation": "EMULAZIONE",
|
||||
"Options.Section.Logging": "LOG",
|
||||
"Options.Section.Launcher": "LAUNCHER",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU engine",
|
||||
"Options.CpuEngine.Desc": "Motore di esecuzione utilizzato per eseguire il codice del gioco.",
|
||||
"Options.CpuEngine.Native": "Nativo",
|
||||
|
||||
"Options.Strict.Label": "Risoluzione rigorosa dynlib",
|
||||
"Options.Strict.Desc": "Interrompi l'avvio quando un simbolo importato non può essere trovato.",
|
||||
|
||||
"Options.LogLevel.Label": "Livello Log",
|
||||
"Options.LogLevel.Desc": "Livello di dettaglio dell'output console dell'emulatore.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warning",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Critical",
|
||||
|
||||
"Options.TraceImports.Label": "Limite tracciamento import",
|
||||
"Options.TraceImports.Desc": "Traccia i primi N import per modulo (0 = disattivato).",
|
||||
|
||||
|
||||
"Options.LogToFile.Label": "Salva log su file",
|
||||
"Options.LogToFile.Desc": "Duplica l'output dell'emulatore in un file di log.",
|
||||
|
||||
|
||||
"Options.LogFilePath.Label": "Percorso file di log",
|
||||
"Options.LogFilePath.Default": "Nessun percorso personalizzato — i log vengono salvati in user/logs accanto all'emulatore.",
|
||||
"Options.LogFilePath.Select": "Seleziona…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Sovrascrivi file di log",
|
||||
"Options.OverrideLogFile.Desc": "Usa esattamente il percorso specificato invece di aggiungere Title ID e timestamp.",
|
||||
|
||||
|
||||
"Options.TitleMusic.Label": "Musica del titolo",
|
||||
"Options.TitleMusic.Desc": "Riproduci in loop la musica di anteprima del gioco selezionato nella libreria.",
|
||||
|
||||
"Options.Discord.Label": "Presenza Discord",
|
||||
"Options.Discord.Desc": "Mostra il gioco in esecuzione sul tuo profilo Discord.",
|
||||
|
||||
"Options.Language.Label": "Lingua dell'emulatore",
|
||||
"Options.Language.Desc": "Lingua utilizzata in tutto il launcher. Viene applicata immediatamente.",
|
||||
|
||||
"Common.On": "On",
|
||||
"Common.Off": "Off",
|
||||
|
||||
"Console.Title": "CONSOLE",
|
||||
"Console.SearchWatermark": "Cerca...",
|
||||
"Console.AutoScroll": "Scorrimento automatico",
|
||||
"Console.Split": "Dividi",
|
||||
"Console.Copy": "Copia",
|
||||
"Console.Clear": "Cancella",
|
||||
"Console.WindowTitle": "Console SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "Nessun gioco selezionato",
|
||||
"Launch.NoGameHint": "Scegli un gioco dalla libreria, oppure apri direttamente un eboot.bin.",
|
||||
"Launch.Idle": "Inattivo",
|
||||
"Launch.Console": "≡ Console",
|
||||
"Launch.Launch": "▶ Avvia",
|
||||
"Launch.Stop": "■ Ferma",
|
||||
"Launch.Running": "In esecuzione — {0}",
|
||||
"Launch.Stopping": "Arresto in corso…",
|
||||
"Launch.Exited": "Terminato con codice {0} ({1})",
|
||||
"Launch.ExeNotFound": "Eseguibile SharpEmu non trovato. Compila prima il progetto SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "File di log: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Impossibile avviare l'emulatore: {0}",
|
||||
"Launch.ProcessExited": "Processo terminato con codice {0} ({1}).",
|
||||
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "argomenti non validi",
|
||||
"Exit.EbootNotFound": "eboot non trovato",
|
||||
"Exit.RuntimeException": "errore di runtime",
|
||||
"Exit.EmulationError": "errore di emulazione",
|
||||
"Exit.Unknown": "sconosciuto",
|
||||
|
||||
|
||||
"Status.EmulatorLocating": "Emulatore: ricerca in corso…",
|
||||
"Status.EmulatorPath": "Emulatore: {0}",
|
||||
"Status.EmulatorNotFound": "Emulatore: eseguibile SharpEmu non trovato — compila prima SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Scansione della libreria…",
|
||||
"Status.AddFolderPrompt": "Aggiungi una cartella di giochi per popolare la libreria.",
|
||||
"Status.LibraryScanned": "Libreria scansionata: {0} gioco/giochi in {1} cartella/e.",
|
||||
"Status.CouldNotOpenFolder": "Impossibile aprire la cartella: {0}",
|
||||
"Status.CopiedToClipboard": "{0} copiato negli appunti.",
|
||||
"Status.RemovedFromLibrary": "“{0}” rimosso dalla libreria. Riaggiungi la sua cartella per ripristinarlo.",
|
||||
"Status.Running": "In esecuzione: {0}",
|
||||
"Status.Stopping": "Arresto in corso…",
|
||||
"Status.Idle": "Inattivo",
|
||||
|
||||
"Clipboard.Path": "Percorso",
|
||||
"Clipboard.TitleId": "ID Titolo",
|
||||
|
||||
"Discord.Playing": "Sta giocando a {0}",
|
||||
"Discord.Browsing": "Sta esplorando la libreria",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Scegli una cartella contenente giochi",
|
||||
"Dialog.OpenExecutable": "Apri un eseguibile da avviare",
|
||||
"Dialog.PsExecutables": "Eseguibili PS",
|
||||
"Dialog.SaveLogFile": "Scegli dove salvare il file di log",
|
||||
"Dialog.PlainTextFiles": "File di testo semplice",
|
||||
"Dialog.LogFiles": "File di log"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "日本語",
|
||||
|
||||
"Page.Library": "ライブラリ",
|
||||
"Page.Options": "オプション",
|
||||
"Page.GameCount.One": "ゲーム 1本",
|
||||
"Page.GameCount.Other": "ゲーム {0}本",
|
||||
|
||||
"Library.SearchWatermark": "ライブラリを検索…",
|
||||
"Library.AddFolder": "+ フォルダーを追加",
|
||||
"Library.Rescan": "⟳ 再スキャン",
|
||||
"Library.OpenFile": "ファイルを開く…",
|
||||
|
||||
"Library.Context.Launch": "起動",
|
||||
"Library.Context.OpenFolder": "ゲームフォルダーを開く",
|
||||
"Library.Context.CopyPath": "パスをコピー",
|
||||
"Library.Context.CopyTitleId": "ゲームIDをコピー",
|
||||
"Library.Context.Remove": "ライブラリから削除",
|
||||
|
||||
"Library.Empty.Title": "ライブラリが空です",
|
||||
"Library.Empty.Hint": "開始するには、ゲームが含まれるフォルダーを追加してください。",
|
||||
"Library.Empty.SearchTitle": "検索条件に一致するゲームが見つかりません",
|
||||
"Library.Empty.SearchHint": "ライブラリに「{0}」と一致する項目はありません。",
|
||||
"Library.Empty.AddFolder": "+ ゲームフォルダーを追加",
|
||||
|
||||
"Library.Loading": "ライブラリを読み込み中…",
|
||||
|
||||
"Options.General": "一般",
|
||||
"Options.Section.Emulation": "エミュレーション",
|
||||
"Options.Section.Logging": "ロギング",
|
||||
"Options.Section.Launcher": "ランチャー",
|
||||
|
||||
"Options.CpuEngine.Label": "CPUエンジン",
|
||||
"Options.CpuEngine.Desc": "ゲームコードを実行するために使用される実行エンジン。",
|
||||
"Options.CpuEngine.Native": "ネイティブ",
|
||||
|
||||
"Options.Strict.Label": "厳格な動的ライブラリ解決",
|
||||
"Options.Strict.Desc": "インポートされたシンボルが解決できない場合、起動を中断します。",
|
||||
|
||||
"Options.LogLevel.Label": "ログレベル",
|
||||
"Options.LogLevel.Desc": "エミュレータコンソールに表示されるメッセージの詳細度。",
|
||||
"Options.LogLevel.Trace": "トレース",
|
||||
"Options.LogLevel.Debug": "デバッグ",
|
||||
"Options.LogLevel.Info": "情報",
|
||||
"Options.LogLevel.Warning": "警告",
|
||||
"Options.LogLevel.Error": "エラー",
|
||||
"Options.LogLevel.Critical": "致命的なエラー",
|
||||
|
||||
"Options.TraceImports.Label": "インポートトレース制限",
|
||||
"Options.TraceImports.Desc": "各モジュールの最初のN個のインポートをトレースします(0 = 無効)。",
|
||||
|
||||
"Options.LogToFile.Label": "ファイルに保存",
|
||||
"Options.LogToFile.Desc": "エミュレータの出力をログファイルにコピーします。",
|
||||
|
||||
"Options.LogFilePath.Label": "ログファイルのパス",
|
||||
"Options.LogFilePath.Default": "カスタムパスなし — ログはエミュレータと同じ場所の user/logs フォルダーに保存されます。",
|
||||
"Options.LogFilePath.Select": "選択…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "ログファイルを上書き",
|
||||
"Options.OverrideLogFile.Desc": "ゲームIDやタイムスタンプを追加せず、指定されたパスをそのまま使用します。",
|
||||
|
||||
"Options.TitleMusic.Label": "ゲーム内音楽",
|
||||
"Options.TitleMusic.Desc": "ライブラリで選択したゲームのプレビュー音楽をループ再生します。",
|
||||
|
||||
"Options.Discord.Label": "Discordステータス表示",
|
||||
"Options.Discord.Desc": "現在プレイ中のゲームをDiscordのプロフィールに表示します。",
|
||||
|
||||
"Options.Language.Label": "エミュレータの言語",
|
||||
"Options.Language.Desc": "ランチャー全体で使用される言語。変更はすぐに適用されます。",
|
||||
|
||||
"Common.On": "オン",
|
||||
"Common.Off": "オフ",
|
||||
|
||||
"Console.Title": "コンソール",
|
||||
"Console.SearchWatermark": "検索…",
|
||||
"Console.AutoScroll": "自動スクロール",
|
||||
"Console.Split": "ウィンドウを分離",
|
||||
"Console.Copy": "コピー",
|
||||
"Console.Clear": "消去",
|
||||
"Console.WindowTitle": "SharpEmu コンソール",
|
||||
|
||||
"Launch.NoGameSelected": "ゲームが選択されていません",
|
||||
"Launch.NoGameHint": "ライブラリからゲームを選択するか、eboot.bin ファイルを直接開いてください。",
|
||||
"Launch.Idle": "待機中",
|
||||
"Launch.Console": "≡ コンソール",
|
||||
"Launch.Launch": "▶ 起動",
|
||||
"Launch.Stop": "■ 停止",
|
||||
"Launch.Running": "実行中 — {0}",
|
||||
"Launch.Stopping": "停止中…",
|
||||
"Launch.Exited": "プロセスがコード {0} ({1}) で終了しました",
|
||||
"Launch.ExeNotFound": "SharpEmuの実行ファイルが見つかりません。先に SharpEmu.CLI プロジェクトをビルドしてください(dotnet build)。",
|
||||
"Launch.LogFile": "ログファイル: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "エミュレータを起動できませんでした: {0}",
|
||||
"Launch.ProcessExited": "プロセスがコード {0} ({1}) で終了しました。",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "無効な引数",
|
||||
"Exit.EbootNotFound": "eboot が見つかりません",
|
||||
"Exit.RuntimeException": "ランタイム例外",
|
||||
"Exit.EmulationError": "エミュレーションエラー",
|
||||
"Exit.Unknown": "不明",
|
||||
|
||||
"Status.EmulatorLocating": "エミュレータ: 位置を検索中…",
|
||||
"Status.EmulatorPath": "エミュレータ: {0}",
|
||||
"Status.EmulatorNotFound": "エミュレータ: SharpEmuの実行ファイルが見つかりません — 先に SharpEmu.CLI をビルドしてください。",
|
||||
"Status.ScanningLibrary": "ライブラリをスキャン中…",
|
||||
"Status.AddFolderPrompt": "ライブラリに表示するゲームフォルダーを追加してください。",
|
||||
"Status.LibraryScanned": "ライブラリのスキャン完了: {1} 個のフォルダーから {0} 本のゲームを検出。",
|
||||
"Status.CouldNotOpenFolder": "フォルダーを開けませんでした: {0}",
|
||||
"Status.CopiedToClipboard": "「{0}」をクリップボードにコピーしました。",
|
||||
"Status.RemovedFromLibrary": "「{0}」がライブラリから削除されました。復元するにはフォルダーを再追加してください。",
|
||||
"Status.Running": "{0} を実行中",
|
||||
"Status.Stopping": "停止中…",
|
||||
"Status.Idle": "待機中",
|
||||
|
||||
"Clipboard.Path": "パス",
|
||||
"Clipboard.TitleId": "ゲームID",
|
||||
|
||||
"Discord.Playing": "{0} をプレイ中",
|
||||
"Discord.Browsing": "ライブラリを閲覧中",
|
||||
|
||||
"Dialog.ChooseGameFolder": "ゲームが含まれるフォルダーを選択",
|
||||
"Dialog.OpenExecutable": "起動する実行ファイルを開く",
|
||||
"Dialog.PsExecutables": "PlayStation 実行ファイル",
|
||||
"Dialog.SaveLogFile": "ログファイルの保存先を選択",
|
||||
"Dialog.PlainTextFiles": "プレーンテキストファイル",
|
||||
"Dialog.LogFiles": "ログファイル"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "한국어",
|
||||
|
||||
"Page.Library": "라이브러리",
|
||||
"Page.Options": "옵션",
|
||||
"Page.GameCount.One": "게임 1개",
|
||||
"Page.GameCount.Other": "게임 {0}개",
|
||||
|
||||
"Library.SearchWatermark": "라이브러리 검색…",
|
||||
"Library.AddFolder": "+ 폴더 추가",
|
||||
"Library.Rescan": "⟳ 다시 스캔",
|
||||
"Library.OpenFile": "파일 열기…",
|
||||
|
||||
"Library.Context.Launch": "실행",
|
||||
"Library.Context.OpenFolder": "게임 폴더 열기",
|
||||
"Library.Context.CopyPath": "경로 복사",
|
||||
"Library.Context.CopyTitleId": "게임 ID 복사",
|
||||
"Library.Context.Remove": "라이브러리에서 제거",
|
||||
|
||||
"Library.Empty.Title": "라이브러리가 비어 있습니다",
|
||||
"Library.Empty.Hint": "시작하려면 게임이 포함된 폴더를 추가하세요.",
|
||||
"Library.Empty.SearchTitle": "검색 결과와 일치하는 게임이 없습니다",
|
||||
"Library.Empty.SearchHint": "라이브러리에 '{0}'와(과) 일치하는 항목이 없습니다.",
|
||||
"Library.Empty.AddFolder": "+ 게임 폴더 추가",
|
||||
|
||||
"Library.Loading": "라이브러리 불러오는 중…",
|
||||
|
||||
"Options.General": "일반",
|
||||
"Options.Section.Emulation": "에뮬레이션",
|
||||
"Options.Section.Logging": "로깅",
|
||||
"Options.Section.Launcher": "런처",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU 엔진",
|
||||
"Options.CpuEngine.Desc": "게임 코드를 실행하는 데 사용되는 실행 엔진입니다.",
|
||||
"Options.CpuEngine.Native": "네이티브",
|
||||
|
||||
"Options.Strict.Label": "엄격한 동적 라이브러리 해석",
|
||||
"Options.Strict.Desc": "가져온 심볼을 해석할 수 없는 경우 실행을 중단합니다.",
|
||||
|
||||
"Options.LogLevel.Label": "로그 수준",
|
||||
"Options.LogLevel.Desc": "에뮬레이터 콘솔에 표시할 메시지의 세부 정보 수준입니다.",
|
||||
"Options.LogLevel.Trace": "트레이스",
|
||||
"Options.LogLevel.Debug": "디버그",
|
||||
"Options.LogLevel.Info": "정보",
|
||||
"Options.LogLevel.Warning": "경고",
|
||||
"Options.LogLevel.Error": "오류",
|
||||
"Options.LogLevel.Critical": "치명적 오류",
|
||||
|
||||
"Options.TraceImports.Label": "가져오기 트레이스 한도",
|
||||
"Options.TraceImports.Desc": "각 모듈의 처음 N개 가져오기를 트레이스합니다 (0 = 비활성화).",
|
||||
|
||||
"Options.LogToFile.Label": "파일로 저장",
|
||||
"Options.LogToFile.Desc": "에뮬레이터 출력을 로그 파일에 복사합니다.",
|
||||
|
||||
"Options.LogFilePath.Label": "로그 파일 경로",
|
||||
"Options.LogFilePath.Default": "사용자 지정 경로 없음 — 로그는 에뮬레이터 옆의 user/logs 폴더에 저장됩니다.",
|
||||
"Options.LogFilePath.Select": "선택…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "로그 파일 덮어쓰기",
|
||||
"Options.OverrideLogFile.Desc": "게임 ID와 타임스탬프를 추가하는 대신 정확히 이 경로를 사용합니다.",
|
||||
|
||||
"Options.TitleMusic.Label": "게임 음악",
|
||||
"Options.TitleMusic.Desc": "라이브러리에서 선택한 게임의 미리보기 음악을 반복 재생합니다.",
|
||||
|
||||
"Options.Discord.Label": "디스코드 상태 표시",
|
||||
"Options.Discord.Desc": "디스코드 프로필에 현재 실행 중인 게임을 표시합니다.",
|
||||
|
||||
"Options.Language.Label": "에뮬레이터 언어",
|
||||
"Options.Language.Desc": "런처 전체에 사용되는 언어입니다. 변경 사항은 즉시 적용됩니다.",
|
||||
|
||||
"Common.On": "켬",
|
||||
"Common.Off": "끔",
|
||||
|
||||
"Console.Title": "콘솔",
|
||||
"Console.SearchWatermark": "검색…",
|
||||
"Console.AutoScroll": "자동 스크롤",
|
||||
"Console.Split": "창 분리",
|
||||
"Console.Copy": "복사",
|
||||
"Console.Clear": "지우기",
|
||||
"Console.WindowTitle": "SharpEmu 콘솔",
|
||||
|
||||
"Launch.NoGameSelected": "선택된 게임 없음",
|
||||
"Launch.NoGameHint": "라이브러리에서 게임을 선택하거나 eboot.bin 파일을 직접 여세요.",
|
||||
"Launch.Idle": "대기 중",
|
||||
"Launch.Console": "≡ 콘솔",
|
||||
"Launch.Launch": "▶ 실행",
|
||||
"Launch.Stop": "■ 중지",
|
||||
"Launch.Running": "실행 중 — {0}",
|
||||
"Launch.Stopping": "중지 중…",
|
||||
"Launch.Exited": "프로세스가 코드 {0} ({1})(으)로 종료되었습니다",
|
||||
"Launch.ExeNotFound": "SharpEmu 실행 파일을 찾을 수 없습니다. 먼저 SharpEmu.CLI 프로젝트를 컴파일하세요 (dotnet build).",
|
||||
"Launch.LogFile": "로그 파일: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "에뮬레이터를 시작할 수 없습니다: {0}",
|
||||
"Launch.ProcessExited": "프로세스가 코드 {0} ({1})(으)로 종료되었습니다.",
|
||||
|
||||
"Exit.Ok": "확인",
|
||||
"Exit.InvalidArguments": "잘못된 인수",
|
||||
"Exit.EbootNotFound": "eboot을 찾을 수 없음",
|
||||
"Exit.RuntimeException": "런타임 예외",
|
||||
"Exit.EmulationError": "에뮬레이션 오류",
|
||||
"Exit.Unknown": "알 수 없음",
|
||||
|
||||
"Status.EmulatorLocating": "에뮬레이터: 위치 검색 중…",
|
||||
"Status.EmulatorPath": "에뮬레이터: {0}",
|
||||
"Status.EmulatorNotFound": "에뮬레이터: SharpEmu 실행 파일을 찾을 수 없습니다 — 먼저 SharpEmu.CLI를 컴파일하세요.",
|
||||
"Status.ScanningLibrary": "라이브러리 스캔 중…",
|
||||
"Status.AddFolderPrompt": "라이브러리를 채우려면 게임 폴더를 추가하세요.",
|
||||
"Status.LibraryScanned": "라이브러리 스캔 완료: {1}개 폴더에서 {0}개 게임 발견.",
|
||||
"Status.CouldNotOpenFolder": "폴더를 열 수 없습니다: {0}",
|
||||
"Status.CopiedToClipboard": "{0}이(가) 클립보드에 복사되었습니다.",
|
||||
"Status.RemovedFromLibrary": "'{0}'이(가) 라이브러리에서 제거되었습니다. 복구하려면 폴더를 다시 추가하세요.",
|
||||
"Status.Running": "{0} 실행 중",
|
||||
"Status.Stopping": "중지 중…",
|
||||
"Status.Idle": "대기 중",
|
||||
|
||||
"Clipboard.Path": "경로",
|
||||
"Clipboard.TitleId": "게임 ID",
|
||||
|
||||
"Discord.Playing": "{0} 플레이 중",
|
||||
"Discord.Browsing": "라이브러리 둘러보는 중",
|
||||
|
||||
"Dialog.ChooseGameFolder": "게임이 포함된 폴더 선택",
|
||||
"Dialog.OpenExecutable": "실행할 파일 열기",
|
||||
"Dialog.PsExecutables": "PlayStation 실행 파일",
|
||||
"Dialog.SaveLogFile": "로그 파일 저장 위치 선택",
|
||||
"Dialog.PlainTextFiles": "일반 텍스트 파일",
|
||||
"Dialog.LogFiles": "로그 파일"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Русский",
|
||||
|
||||
"Page.Library": "Библиотека",
|
||||
"Page.Options": "Настройки",
|
||||
"Page.GameCount.One": "1 игра",
|
||||
"Page.GameCount.Other": "Игр: {0}",
|
||||
|
||||
"Library.SearchWatermark": "Поиск…",
|
||||
"Library.AddFolder": "+ Добавить папку",
|
||||
"Library.Rescan": "⟳ Сканировать",
|
||||
"Library.OpenFile": "Открыть файл…",
|
||||
|
||||
"Library.Context.Launch": "Запустить",
|
||||
"Library.Context.OpenFolder": "Открыть папку с игрой",
|
||||
"Library.Context.CopyPath": "Скопировать путь",
|
||||
"Library.Context.CopyTitleId": "Скопировать ID игры",
|
||||
"Library.Context.Remove": "Удалить из библиотеки",
|
||||
|
||||
"Library.Empty.Title": "Ваша библиотека пуста",
|
||||
"Library.Empty.Hint": "Добавьте папку с играми, чтобы начать.",
|
||||
"Library.Empty.SearchTitle": "По вашему запросу ничего не найдено",
|
||||
"Library.Empty.SearchHint": "В библиотеке нет игр, соответствующих запросу «{0}».",
|
||||
"Library.Empty.AddFolder": "+ Добавить папку с играми",
|
||||
|
||||
"Library.Loading": "Загрузка библиотеки…",
|
||||
|
||||
"Options.General": "Основные",
|
||||
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
|
||||
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
|
||||
"Options.Section.Launcher": "ЛАУНЧЕР",
|
||||
|
||||
"Options.CpuEngine.Label": "Движок ЦП",
|
||||
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
|
||||
"Options.CpuEngine.Native": "Нативный",
|
||||
|
||||
"Options.Strict.Label": "Строгое разрешение динамических библиотек",
|
||||
"Options.Strict.Desc": "Не запускать игру, если не удается найти или связать импортируемую функцию/переменную.",
|
||||
|
||||
"Options.LogLevel.Label": "Уровень логгирования",
|
||||
"Options.LogLevel.Desc": "Подробность вывода в консоль эмулятора.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warning",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Critical",
|
||||
|
||||
"Options.TraceImports.Label": "Лимит трассировки импортов",
|
||||
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
|
||||
|
||||
"Options.LogToFile.Label": "Запись лога в файл",
|
||||
"Options.LogToFile.Desc": "Дублировать вывод эмулятора в файл лога.",
|
||||
|
||||
"Options.LogFilePath.Label": "Путь к файлу лога",
|
||||
"Options.LogFilePath.Default": "Пользовательский путь не задан: логи сохраняются в user/logs рядом с эмулятором.",
|
||||
"Options.LogFilePath.Select": "Выбрать…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Переопределить файл лога",
|
||||
"Options.OverrideLogFile.Desc": "Использовать указанный путь к файлу без добавления ID игры и метки времени.",
|
||||
|
||||
"Options.TitleMusic.Label": "Музыка игры",
|
||||
"Options.TitleMusic.Desc": "Зацикленно воспроизводить музыку предпросмотра выбранной игры в библиотеке.",
|
||||
|
||||
"Options.Discord.Label": "Статус Discord",
|
||||
"Options.Discord.Desc": "Показывать запущенную игру в профиле Discord.",
|
||||
|
||||
"Options.Language.Label": "Язык эмулятора",
|
||||
"Options.Language.Desc": "Язык интерфейса лаунчера. Изменение применяется сразу.",
|
||||
|
||||
"Common.On": "Включено",
|
||||
"Common.Off": "Выключено",
|
||||
|
||||
"Console.Title": "КОНСОЛЬ",
|
||||
"Console.SearchWatermark": "Поиск...",
|
||||
"Console.AutoScroll": "Авто-прокрутка",
|
||||
"Console.Split": "Разделить",
|
||||
"Console.Copy": "Скопировать",
|
||||
"Console.Clear": "Очистить",
|
||||
"Console.WindowTitle": "Консоль SharpEmu",
|
||||
|
||||
"Launch.NoGameSelected": "Ничего не выбрано",
|
||||
"Launch.NoGameHint": "Выберите игру из библиотеки или откройте eboot.bin напрямую.",
|
||||
"Launch.Idle": "Ожидание",
|
||||
"Launch.Console": "≡ Консоль",
|
||||
"Launch.Launch": "▶ Запустить",
|
||||
"Launch.Stop": "■ Остановить",
|
||||
"Launch.Running": "Запущено - {0}",
|
||||
"Launch.Stopping": "Остановка…",
|
||||
"Launch.Exited": "Завершено с кодом {0} ({1})",
|
||||
"Launch.ExeNotFound": "Исполняемый файл SharpEmu не найден. Скомпилируйте сначала проект SharpEmu.CLI (dotnet build).",
|
||||
"Launch.LogFile": "Лог: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Не удалось запустить эмулятор: {0}",
|
||||
"Launch.ProcessExited": "Процесс завершился с кодом {0} ({1}).",
|
||||
|
||||
"Exit.Ok": "OK",
|
||||
"Exit.InvalidArguments": "некорректные аргументы",
|
||||
"Exit.EbootNotFound": "eboot не найден",
|
||||
"Exit.RuntimeException": "ошибка выполнения",
|
||||
"Exit.EmulationError": "ошибка эмуляции",
|
||||
"Exit.Unknown": "неизвестная ошибка",
|
||||
|
||||
"Status.EmulatorLocating": "Эмулятор: поиск…",
|
||||
"Status.EmulatorPath": "Эмулятор: {0}",
|
||||
"Status.EmulatorNotFound": "Эмулятор: исполняемый файл SharpEmu не был найден - скомпилируйте сначала SharpEmu.CLI.",
|
||||
"Status.ScanningLibrary": "Сканирование библиотеки…",
|
||||
"Status.AddFolderPrompt": "Добавьте папку с играми, чтобы заполнить библиотеку.",
|
||||
"Status.LibraryScanned": "Сканирование библиотеки завершено: игр: {0}, папок: {1}.",
|
||||
"Status.CouldNotOpenFolder": "Не удалось открыть папку: {0}",
|
||||
"Status.CopiedToClipboard": "{0}: скопировано в буфер обмена.",
|
||||
"Status.RemovedFromLibrary": "Игра «{0}» удалена из библиотеки. Добавьте её папку заново, чтобы вернуть.",
|
||||
"Status.Running": "Запущено: {0}",
|
||||
"Status.Stopping": "Остановка…",
|
||||
"Status.Idle": "Ожидание",
|
||||
|
||||
"Clipboard.Path": "Путь",
|
||||
"Clipboard.TitleId": "ID игры",
|
||||
|
||||
"Discord.Playing": "Играет в {0}",
|
||||
"Discord.Browsing": "Просматривает библиотеку",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Выберите папку, содержащую игры",
|
||||
"Dialog.OpenExecutable": "Открыть исполняемый файл для запуска",
|
||||
"Dialog.PsExecutables": "Исполняемые файлы PS",
|
||||
"Dialog.SaveLogFile": "Выберите, куда сохранить файл с логами",
|
||||
"Dialog.PlainTextFiles": "Текстовые файлы",
|
||||
"Dialog.LogFiles": "Логи"
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
{
|
||||
"_languageName": "Türkçe",
|
||||
|
||||
"Page.Library": "Kütüphane",
|
||||
"Page.Options": "Ayarlar",
|
||||
"Page.GameCount.One": "1 oyun",
|
||||
"Page.GameCount.Other": "{0} oyun",
|
||||
|
||||
"Library.SearchWatermark": "Kütüphanede ara…",
|
||||
"Library.AddFolder": "+ Klasör ekle",
|
||||
"Library.Rescan": "⟳ Yeniden tara",
|
||||
"Library.OpenFile": "Dosya aç…",
|
||||
|
||||
"Library.Context.Launch": "Başlat",
|
||||
"Library.Context.OpenFolder": "Oyun klasörünü aç",
|
||||
"Library.Context.CopyPath": "Yolu kopyala",
|
||||
"Library.Context.CopyTitleId": "Title ID'yi kopyala",
|
||||
"Library.Context.Remove": "Kütüphaneden kaldır",
|
||||
|
||||
"Library.Empty.Title": "Kütüphaneniz boş",
|
||||
"Library.Empty.Hint": "Başlamak için oyunlarınızı içeren bir klasör ekleyin.",
|
||||
"Library.Empty.SearchTitle": "Aramanızla eşleşen oyun yok",
|
||||
"Library.Empty.SearchHint": "Kütüphanede “{0}” ile eşleşen bir şey yok.",
|
||||
"Library.Empty.AddFolder": "+ Oyun klasörü ekle",
|
||||
|
||||
"Library.Loading": "Kütüphane yükleniyor…",
|
||||
|
||||
"Options.General": "Genel",
|
||||
"Options.Section.Emulation": "EMÜLASYON",
|
||||
"Options.Section.Logging": "GÜNLÜKLEME",
|
||||
"Options.Section.Launcher": "BAŞLATICI",
|
||||
|
||||
"Options.CpuEngine.Label": "CPU motoru",
|
||||
"Options.CpuEngine.Desc": "Oyun kodunu çalıştırmak için kullanılan yürütme motoru.",
|
||||
"Options.CpuEngine.Native": "Native",
|
||||
|
||||
"Options.Strict.Label": "Katı dynlib çözümü",
|
||||
"Options.Strict.Desc": "İçe aktarılan bir sembol çözümlenemediğinde başlatmayı başarısız kılar.",
|
||||
|
||||
"Options.LogLevel.Label": "Günlük seviyesi",
|
||||
"Options.LogLevel.Desc": "Emülatör konsol çıktısının ayrıntı düzeyi.",
|
||||
"Options.LogLevel.Trace": "Trace",
|
||||
"Options.LogLevel.Debug": "Debug",
|
||||
"Options.LogLevel.Info": "Info",
|
||||
"Options.LogLevel.Warning": "Warning",
|
||||
"Options.LogLevel.Error": "Error",
|
||||
"Options.LogLevel.Critical": "Critical",
|
||||
|
||||
"Options.TraceImports.Label": "Import izleme sınırı",
|
||||
"Options.TraceImports.Desc": "Modül başına ilk N import'u izle (0 = kapalı).",
|
||||
|
||||
"Options.LogToFile.Label": "Dosyaya günlükle",
|
||||
"Options.LogToFile.Desc": "Emülatör çıktısını bir günlük dosyasına yansıt.",
|
||||
|
||||
"Options.LogFilePath.Label": "Günlük dosyası yolu",
|
||||
"Options.LogFilePath.Default": "Özel yol yok — günlükler emülatörün yanındaki user/logs klasörüne yazılır.",
|
||||
"Options.LogFilePath.Select": "Seç…",
|
||||
|
||||
"Options.OverrideLogFile.Label": "Günlük dosyasının üzerine yaz",
|
||||
"Options.OverrideLogFile.Desc": "Title ID ve zaman damgası eklemek yerine tam dosya yolunu kullan.",
|
||||
|
||||
"Options.TitleMusic.Label": "Oyun müziği",
|
||||
"Options.TitleMusic.Desc": "Kütüphanede seçili oyunun önizleme müziğini döngüye al.",
|
||||
|
||||
"Options.Discord.Label": "Discord durumu",
|
||||
"Options.Discord.Desc": "Discord profilinde çalışan oyunu göster.",
|
||||
|
||||
"Options.Language.Label": "Emülatör dili",
|
||||
"Options.Language.Desc": "Başlatıcı genelinde kullanılan dil. Hemen uygulanır.",
|
||||
|
||||
"Common.On": "Açık",
|
||||
"Common.Off": "Kapalı",
|
||||
|
||||
"Console.Title": "KONSOL",
|
||||
"Console.SearchWatermark": "Ara...",
|
||||
"Console.AutoScroll": "Otomatik kaydır",
|
||||
"Console.Split": "Ayır",
|
||||
"Console.Copy": "Kopyala",
|
||||
"Console.Clear": "Temizle",
|
||||
"Console.WindowTitle": "SharpEmu Konsol",
|
||||
|
||||
"Launch.NoGameSelected": "Oyun seçilmedi",
|
||||
"Launch.NoGameHint": "Kütüphaneden bir oyun seçin veya doğrudan bir eboot.bin açın.",
|
||||
"Launch.Idle": "Boşta",
|
||||
"Launch.Console": "≡ Konsol",
|
||||
"Launch.Launch": "▶ Başlat",
|
||||
"Launch.Stop": "■ Durdur",
|
||||
"Launch.Running": "Çalışıyor — {0}",
|
||||
"Launch.Stopping": "Durduruluyor…",
|
||||
"Launch.Exited": "Çıkış kodu {0} ({1})",
|
||||
"Launch.ExeNotFound": "SharpEmu çalıştırılabilir dosyası bulunamadı. Önce SharpEmu.CLI projesini derleyin (dotnet build).",
|
||||
"Launch.LogFile": "Günlük dosyası: {0}",
|
||||
"Launch.Command": "$ SharpEmu {0}",
|
||||
"Launch.StartFailed": "Emülatör başlatılamadı: {0}",
|
||||
"Launch.ProcessExited": "İşlem {0} kodla sonlandı ({1}).",
|
||||
|
||||
"Exit.Ok": "Tamam",
|
||||
"Exit.InvalidArguments": "geçersiz argümanlar",
|
||||
"Exit.EbootNotFound": "eboot bulunamadı",
|
||||
"Exit.RuntimeException": "çalışma zamanı hatası",
|
||||
"Exit.EmulationError": "emülasyon hatası",
|
||||
"Exit.Unknown": "bilinmiyor",
|
||||
|
||||
"Status.EmulatorLocating": "Emülatör: bulunuyor…",
|
||||
"Status.EmulatorPath": "Emülatör: {0}",
|
||||
"Status.EmulatorNotFound": "Emülatör: SharpEmu çalıştırılabilir dosyası bulunamadı — önce SharpEmu.CLI'yi derleyin.",
|
||||
"Status.ScanningLibrary": "Kütüphane taranıyor…",
|
||||
"Status.AddFolderPrompt": "Kütüphaneyi doldurmak için bir oyun klasörü ekleyin.",
|
||||
"Status.LibraryScanned": "Kütüphane tarandı: {1} klasörde {0} oyun.",
|
||||
"Status.CouldNotOpenFolder": "Klasör açılamadı: {0}",
|
||||
"Status.CopiedToClipboard": "{0} panoya kopyalandı.",
|
||||
"Status.RemovedFromLibrary": "“{0}” kütüphaneden kaldırıldı. Geri getirmek için klasörünü yeniden ekleyin.",
|
||||
"Status.Running": "{0} çalışıyor",
|
||||
"Status.Stopping": "Durduruluyor…",
|
||||
"Status.Idle": "Boşta",
|
||||
|
||||
"Clipboard.Path": "Yol",
|
||||
"Clipboard.TitleId": "Title ID",
|
||||
|
||||
"Discord.Playing": "{0} oynuyor",
|
||||
"Discord.Browsing": "Kütüphaneye göz atıyor",
|
||||
|
||||
"Dialog.ChooseGameFolder": "Oyunlarınızı içeren bir klasör seçin",
|
||||
"Dialog.OpenExecutable": "Başlatılacak bir çalıştırılabilir dosya aç",
|
||||
"Dialog.PsExecutables": "PS çalıştırılabilirleri",
|
||||
"Dialog.SaveLogFile": "Günlük dosyasının kaydedileceği yeri seçin",
|
||||
"Dialog.PlainTextFiles": "Düz Metin Dosyaları",
|
||||
"Dialog.LogFiles": "Günlük Dosyaları"
|
||||
}
|
||||
@@ -0,0 +1,256 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Text.Json;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Loads UI strings for the launcher. Every language ships embedded in the
|
||||
/// assembly (see SharpEmu.GUI.csproj) so a release build is fully
|
||||
/// self-contained; an optional Languages/<code>.json file next to the
|
||||
/// executable overrides the embedded copy for that code, so a translation
|
||||
/// fix or a brand-new language never needs a rebuild.
|
||||
/// </summary>
|
||||
public sealed class Localization
|
||||
{
|
||||
public static Localization Instance { get; } = new();
|
||||
|
||||
public sealed record LanguageInfo(string Code, string NativeName);
|
||||
|
||||
private const string EmbeddedResourcePrefix = "Languages.";
|
||||
private const string EmbeddedResourceSuffix = ".json";
|
||||
|
||||
private Dictionary<string, string> _strings = new();
|
||||
private Dictionary<string, string> _fallbackStrings = new();
|
||||
|
||||
|
||||
private Localization()
|
||||
{
|
||||
}
|
||||
|
||||
/// <summary>Directory holding optional *.json language overrides, next to the executable.</summary>
|
||||
public static string LanguagesDirectory => Path.Combine(AppContext.BaseDirectory, "Languages");
|
||||
|
||||
public string CurrentCode { get; private set; } = "en";
|
||||
|
||||
public string Get(string key)
|
||||
{
|
||||
if (_strings.TryGetValue(key, out var value))
|
||||
return value;
|
||||
|
||||
if (_fallbackStrings.TryGetValue(key, out var fallbackValue))
|
||||
return fallbackValue;
|
||||
|
||||
return key;
|
||||
}
|
||||
|
||||
public string Format(string key, params object?[] args) => string.Format(Get(key), args);
|
||||
|
||||
/// <summary>
|
||||
/// Languages available either embedded in the binary or as a loose
|
||||
/// override file, sorted by code. A loose file's declared name wins when
|
||||
/// the same code exists in both places.
|
||||
/// </summary>
|
||||
public List<LanguageInfo> DiscoverLanguages()
|
||||
{
|
||||
var languages = new Dictionary<string, LanguageInfo>(StringComparer.OrdinalIgnoreCase);
|
||||
|
||||
foreach (var code in EmbeddedLanguageCodes())
|
||||
{
|
||||
using var stream = OpenEmbeddedLanguageStream(code);
|
||||
if (stream is not null)
|
||||
{
|
||||
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
|
||||
}
|
||||
}
|
||||
|
||||
try
|
||||
{
|
||||
foreach (var file in Directory.EnumerateFiles(LanguagesDirectory, "*.json"))
|
||||
{
|
||||
var code = Path.GetFileNameWithoutExtension(file);
|
||||
using var stream = File.OpenRead(file);
|
||||
languages[code] = new LanguageInfo(code, ReadLanguageName(stream) ?? code);
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// No loose Languages directory: the embedded languages still stand.
|
||||
}
|
||||
|
||||
var result = languages.Values.ToList();
|
||||
result.Sort((a, b) => string.CompareOrdinal(a.Code, b.Code));
|
||||
return result;
|
||||
}
|
||||
|
||||
/// <summary>Loads a language by code (e.g. "en"): a loose override file first, then the embedded copy.</summary>
|
||||
/// english is the fallback language
|
||||
public void Load(string code)
|
||||
{
|
||||
if (_fallbackStrings.Count == 0 && !string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (!TryLoadLooseFile("en", out var fallback) && !TryLoadEmbedded("en", out fallback))
|
||||
{
|
||||
fallback = new Dictionary<string, string>();
|
||||
}
|
||||
_fallbackStrings = fallback;
|
||||
}
|
||||
else if (string.Equals(code, "en", StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
if (TryLoadLooseFile("en", out var enDict) || TryLoadEmbedded("en", out enDict))
|
||||
{
|
||||
_strings = enDict;
|
||||
_fallbackStrings = enDict;
|
||||
}
|
||||
else
|
||||
{
|
||||
_strings = new Dictionary<string, string>();
|
||||
_fallbackStrings = new Dictionary<string, string>();
|
||||
}
|
||||
CurrentCode = "en";
|
||||
return;
|
||||
}
|
||||
|
||||
// Load the requested language
|
||||
if (TryLoadLooseFile(code, out var loaded) || TryLoadEmbedded(code, out loaded))
|
||||
{
|
||||
_strings = loaded;
|
||||
}
|
||||
else
|
||||
{
|
||||
if (_fallbackStrings.Count > 0)
|
||||
_strings = new Dictionary<string, string>(_fallbackStrings);
|
||||
else
|
||||
_strings = new Dictionary<string, string>();
|
||||
}
|
||||
CurrentCode = code;
|
||||
}
|
||||
|
||||
private static IEnumerable<string> EmbeddedLanguageCodes()
|
||||
{
|
||||
foreach (var name in typeof(Localization).Assembly.GetManifestResourceNames())
|
||||
{
|
||||
if (name.StartsWith(EmbeddedResourcePrefix, StringComparison.Ordinal) &&
|
||||
name.EndsWith(EmbeddedResourceSuffix, StringComparison.Ordinal))
|
||||
{
|
||||
yield return name[EmbeddedResourcePrefix.Length..^EmbeddedResourceSuffix.Length];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private static Stream? OpenEmbeddedLanguageStream(string code) =>
|
||||
typeof(Localization).Assembly.GetManifestResourceStream($"{EmbeddedResourcePrefix}{code}{EmbeddedResourceSuffix}");
|
||||
|
||||
private static string? ReadLanguageName(Stream stream)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var document = JsonDocument.Parse(stream);
|
||||
if (document.RootElement.TryGetProperty("_languageName", out var name) &&
|
||||
name.ValueKind == JsonValueKind.String)
|
||||
{
|
||||
return name.GetString();
|
||||
}
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
// Malformed file: fall back to the code as its own display name.
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
private bool TryLoadLooseFile(string code)
|
||||
{
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
||||
return File.Exists(path) && TryLoad(code, File.ReadAllText(path));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoadEmbedded(string code)
|
||||
{
|
||||
try
|
||||
{
|
||||
using var stream = OpenEmbeddedLanguageStream(code);
|
||||
if (stream is null)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
return TryLoad(code, reader.ReadToEnd());
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoadLooseFile(string code, out Dictionary<string, string> result)
|
||||
{
|
||||
result = new Dictionary<string, string>();
|
||||
try
|
||||
{
|
||||
var path = Path.Combine(LanguagesDirectory, $"{code}.json");
|
||||
if (!File.Exists(path))
|
||||
return false;
|
||||
|
||||
var json = File.ReadAllText(path);
|
||||
return TryLoad(json, out result);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private bool TryLoadEmbedded(string code, out Dictionary<string, string> result)
|
||||
{
|
||||
result = new Dictionary<string, string>();
|
||||
try
|
||||
{
|
||||
using var stream = OpenEmbeddedLanguageStream(code);
|
||||
if (stream is null)
|
||||
return false;
|
||||
|
||||
using var reader = new StreamReader(stream);
|
||||
var json = reader.ReadToEnd();
|
||||
return TryLoad(json, out result);
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
private static bool TryLoad(string json, out Dictionary<string, string> result)
|
||||
{
|
||||
var loaded = JsonSerializer.Deserialize<Dictionary<string, string>>(json);
|
||||
if (loaded is null)
|
||||
{
|
||||
result = new Dictionary<string, string>();
|
||||
return false;
|
||||
}
|
||||
|
||||
result = loaded;
|
||||
return true;
|
||||
}
|
||||
|
||||
private bool TryLoad(string code, string json)
|
||||
{
|
||||
if (TryLoad(json, out var dict))
|
||||
{
|
||||
_strings = dict;
|
||||
CurrentCode = code;
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,8 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using Avalonia.Media;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
public sealed record LogLine(string Text, IBrush Brush);
|
||||
@@ -0,0 +1,432 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Window xmlns="https://github.com/avaloniaui"
|
||||
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
|
||||
x:Class="SharpEmu.GUI.MainWindow"
|
||||
Title="SharpEmu"
|
||||
Width="1280" Height="820"
|
||||
MinWidth="980" MinHeight="640"
|
||||
WindowState="Maximized"
|
||||
WindowStartupLocation="CenterScreen"
|
||||
Background="{StaticResource BgBrush}"
|
||||
ExtendClientAreaToDecorationsHint="True"
|
||||
ExtendClientAreaChromeHints="PreferSystemChrome"
|
||||
ExtendClientAreaTitleBarHeightHint="44"
|
||||
Icon="avares://SharpEmu.GUI/Assets/SharpEmu.ico"
|
||||
KeyDown="OnKeyDown">
|
||||
|
||||
<Grid RowDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- Selected-game backdrop: key art behind the main content, dimmed by
|
||||
a scrim so tiles and text stay readable. Fades on selection. -->
|
||||
<Panel Grid.Row="1" ClipToBounds="True">
|
||||
<Image x:Name="BackdropImage" Stretch="UniformToFill" Opacity="0"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center">
|
||||
<Image.Transitions>
|
||||
<Transitions>
|
||||
<DoubleTransition Property="Opacity" Duration="0:0:0.35" />
|
||||
</Transitions>
|
||||
</Image.Transitions>
|
||||
</Image>
|
||||
<Border>
|
||||
<Border.Background>
|
||||
<LinearGradientBrush StartPoint="0%,0%" EndPoint="0%,100%">
|
||||
<GradientStop Offset="0" Color="#730D1017" />
|
||||
<GradientStop Offset="0.55" Color="#BF0D1017" />
|
||||
<GradientStop Offset="1" Color="#EB0D1017" />
|
||||
</LinearGradientBrush>
|
||||
</Border.Background>
|
||||
</Border>
|
||||
</Panel>
|
||||
|
||||
<!-- Title bar; hidden in fullscreen (F11) along with the status bar, so
|
||||
the game gets the whole screen. -->
|
||||
<Grid x:Name="TitleBar" Grid.Row="0" Height="44" Background="{StaticResource ChromeBrush}">
|
||||
<StackPanel Orientation="Horizontal" Spacing="10" Margin="16,0" VerticalAlignment="Center">
|
||||
<Image Source="avares://SharpEmu.GUI/Assets/SharpEmu.ico" Width="20" Height="20"
|
||||
RenderOptions.BitmapInterpolationMode="HighQuality" />
|
||||
<TextBlock Text="SharpEmu" FontSize="14" FontWeight="SemiBold" VerticalAlignment="Center" />
|
||||
<Border Classes="pill">
|
||||
<TextBlock x:Name="VersionText" Text="v0.0.1" FontSize="11" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<!-- Main content -->
|
||||
<Grid Grid.Row="1" Margin="32,24,32,20" RowDefinitions="Auto,*,Auto,Auto">
|
||||
|
||||
<!-- Library / Options page switcher, with the library toolbar sharing
|
||||
the same row on the right. Plain buttons (not TabItem) so there is
|
||||
no underline; LB/RB hint chips flank the pair and the gamepad's
|
||||
shoulder buttons actually switch pages from anywhere. -->
|
||||
<Grid Grid.Row="0" ColumnDefinitions="Auto,*,Auto" Margin="0,0,0,20">
|
||||
<StackPanel Grid.Column="0" Orientation="Horizontal" Spacing="14" VerticalAlignment="Center">
|
||||
<Border Classes="padHint" VerticalAlignment="Center">
|
||||
<TextBlock Text="LB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<Button x:Name="LibraryTabButton" Classes="segment active" Content="Library" />
|
||||
<Button x:Name="OptionsTabButton" Classes="segment" Content="Options" />
|
||||
<Border Classes="padHint" VerticalAlignment="Center">
|
||||
<TextBlock Text="RB" FontSize="11" FontWeight="Bold" Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2" x:Name="LibraryToolbar" Orientation="Horizontal" Spacing="8"
|
||||
VerticalAlignment="Center">
|
||||
<TextBox x:Name="SearchBox" Watermark="Search library…" Width="240" VerticalAlignment="Center" />
|
||||
<Button x:Name="AddFolderButton" Classes="ghost" Content="+ Add folder" VerticalAlignment="Center" />
|
||||
<Button x:Name="RescanButton" Classes="ghost" Content="⟳ Rescan" VerticalAlignment="Center" />
|
||||
<Button x:Name="OpenFileButton" Classes="ghost" Content="Open file…" VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<Panel Grid.Row="1">
|
||||
|
||||
<!-- Library page. The tile row gets extra top margin so it sits
|
||||
closer to eye level (PS5 home-screen style) instead of hugging
|
||||
the Library/Options switcher above it. -->
|
||||
<Panel x:Name="LibraryPage" Margin="0,64,0,0">
|
||||
<ListBox x:Name="GameList" Classes="tileGrid" Background="Transparent"
|
||||
SelectionMode="Single" Padding="0">
|
||||
<ListBox.ContextMenu>
|
||||
<ContextMenu x:Name="GameContextMenu" Placement="Pointer">
|
||||
<MenuItem x:Name="CtxLaunch" Header="Launch" FontWeight="SemiBold">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="▶" FontSize="11" Foreground="{StaticResource AccentHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxOpenFolder" Header="Open game folder">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="📂" FontSize="12" HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxCopyPath" Header="Copy path">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<MenuItem x:Name="CtxCopyTitleId" Header="Copy title ID">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="⧉" FontSize="13" Foreground="{StaticResource MutedBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
<Separator />
|
||||
<MenuItem x:Name="CtxRemove" Header="Remove from library"
|
||||
Foreground="{StaticResource DangerHoverBrush}">
|
||||
<MenuItem.Icon>
|
||||
<TextBlock Text="✕" FontSize="12" Foreground="{StaticResource DangerHoverBrush}"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</MenuItem.Icon>
|
||||
</MenuItem>
|
||||
</ContextMenu>
|
||||
</ListBox.ContextMenu>
|
||||
<ListBox.ItemsPanel>
|
||||
<ItemsPanelTemplate>
|
||||
<WrapPanel />
|
||||
</ItemsPanelTemplate>
|
||||
</ListBox.ItemsPanel>
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<StackPanel Width="128" Height="172" Spacing="7">
|
||||
<Border Classes="coverShadow" Width="128" Height="128">
|
||||
<Border Classes="coverClip">
|
||||
<Panel>
|
||||
<Border Background="{Binding PlaceholderBrush}" IsVisible="{Binding !HasCover}">
|
||||
<TextBlock Text="{Binding Initials}" FontSize="36" FontWeight="Bold"
|
||||
Foreground="#E8ECF4" Opacity="0.85"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Image Source="{Binding Cover}" Stretch="UniformToFill" IsVisible="{Binding HasCover}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
</Border>
|
||||
<TextBlock Text="{Binding Name}" FontSize="13" FontWeight="SemiBold"
|
||||
TextWrapping="Wrap" MaxLines="2" TextTrimming="CharacterEllipsis"
|
||||
TextAlignment="Center" HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
|
||||
<!-- Empty state -->
|
||||
<StackPanel x:Name="EmptyState" Spacing="10" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="False">
|
||||
<TextBlock Text="🎮" FontSize="44" HorizontalAlignment="Center" Opacity="0.7" />
|
||||
<TextBlock x:Name="EmptyStateTitle" Text="Your library is empty" FontSize="18" FontWeight="SemiBold"
|
||||
HorizontalAlignment="Center" />
|
||||
<TextBlock x:Name="EmptyStateHint" Text="Add a folder containing your games to get started."
|
||||
FontSize="13" Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||
<Button x:Name="EmptyAddFolderButton" Classes="accent" Content="+ Add game folder"
|
||||
HorizontalAlignment="Center" Margin="0,8,0,0" />
|
||||
</StackPanel>
|
||||
|
||||
<!-- Loading state: covers the grid while a scan (initial load,
|
||||
rescan, or a just-added folder) is in flight, so the screen
|
||||
never looks blank mid-scan. -->
|
||||
<StackPanel x:Name="LoadingState" Spacing="14" HorizontalAlignment="Center" VerticalAlignment="Center"
|
||||
IsVisible="False">
|
||||
<ProgressBar IsIndeterminate="True" Width="180" Height="3" />
|
||||
<TextBlock x:Name="LoadingStateText" Text="Loading library…" FontSize="13"
|
||||
Foreground="{StaticResource MutedBrush}" HorizontalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</Panel>
|
||||
|
||||
<!-- Options page: sub-tabs so future categories (Graphics, …) slot
|
||||
in next to General. Card-grouped sections match the visual
|
||||
language used by the console panel and launch bar below. -->
|
||||
<Grid x:Name="OptionsPage" IsVisible="False">
|
||||
<TabControl>
|
||||
<TabItem x:Name="GeneralTabItem" Header="General" FontSize="15">
|
||||
<ScrollViewer>
|
||||
<StackPanel Margin="0,14,0,16" Spacing="16" MaxWidth="1280" HorizontalAlignment="Left">
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle" Text="EMULATION" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="CpuEngineLabel" Text="CPU engine" FontSize="13" />
|
||||
<TextBlock x:Name="CpuEngineDesc" Text="Execution engine used to run game code."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="CpuEngineBox" Width="160" SelectedIndex="0"
|
||||
VerticalAlignment="Center" CornerRadius="8">
|
||||
<ComboBoxItem x:Name="CpuEngineNativeItem" Content="Native" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="StrictLabel" Text="Strict dynlib resolution" FontSize="13" />
|
||||
<TextBlock x:Name="StrictDesc" Text="Fail the launch when an imported symbol cannot be resolved."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="StrictToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle" Text="LOGGING" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LogLevelLabel" Text="Log level" FontSize="13" />
|
||||
<TextBlock x:Name="LogLevelDesc" Text="Verbosity of the emulator console output."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="LogLevelBox" Width="160" SelectedIndex="2"
|
||||
VerticalAlignment="Center" CornerRadius="8">
|
||||
<ComboBoxItem x:Name="LogLevelTraceItem" Content="Trace" />
|
||||
<ComboBoxItem x:Name="LogLevelDebugItem" Content="Debug" />
|
||||
<ComboBoxItem x:Name="LogLevelInfoItem" Content="Info" />
|
||||
<ComboBoxItem x:Name="LogLevelWarningItem" Content="Warning" />
|
||||
<ComboBoxItem x:Name="LogLevelErrorItem" Content="Error" />
|
||||
<ComboBoxItem x:Name="LogLevelCriticalItem" Content="Critical" />
|
||||
</ComboBox>
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="TraceImportsLabel" Text="Import trace limit" FontSize="13" />
|
||||
<TextBlock x:Name="TraceImportsDesc" Text="Trace the first N imports per module (0 = off)."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<NumericUpDown Grid.Column="1" x:Name="TraceImportsBox" Width="160" Minimum="0"
|
||||
Maximum="4096" Increment="16" Value="0" FormatString="0"
|
||||
VerticalAlignment="Center" CornerRadius="8" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LogToFileLabel" Text="Log to file" FontSize="13" />
|
||||
<TextBlock x:Name="LogToFileDesc" Text="Mirror emulator output to a log file."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="LogToFileToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LogFilePathLabel" Text="Log file path" FontSize="13" />
|
||||
<TextBlock x:Name="LogFilePathText" Text="No custom path"
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<Button Grid.Column="1" x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="OverrideLogFileLabel" Text="Override log file" FontSize="13" />
|
||||
<TextBlock x:Name="OverrideLogFileDesc"
|
||||
Text="Use the exact file path instead of appending title ID and timestamp."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
<Border Classes="card">
|
||||
<StackPanel Spacing="14">
|
||||
<TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle" Text="LAUNCHER" />
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="LanguageLabel" Text="Emulator language" FontSize="13" />
|
||||
<TextBlock x:Name="LanguageDesc"
|
||||
Text="Language used throughout the launcher. Applies immediately."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ComboBox Grid.Column="1" x:Name="LanguageBox" Width="160"
|
||||
VerticalAlignment="Center" CornerRadius="8"
|
||||
DisplayMemberBinding="{Binding NativeName}" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="TitleMusicLabel" Text="Title music" FontSize="13" />
|
||||
<TextBlock x:Name="TitleMusicDesc" Text="Loop the selected game's preview music in the library."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="TitleMusicToggle" OnContent="On" OffContent="Off"
|
||||
IsChecked="True" VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
|
||||
<Grid ColumnDefinitions="*,Auto">
|
||||
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
|
||||
<TextBlock x:Name="DiscordLabel" Text="Discord presence" FontSize="13" />
|
||||
<TextBlock x:Name="DiscordDesc" Text="Show the running game on your Discord profile."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
|
||||
</StackPanel>
|
||||
<ToggleSwitch Grid.Column="1" x:Name="DiscordToggle" OnContent="On" OffContent="Off"
|
||||
VerticalAlignment="Center" />
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
|
||||
</StackPanel>
|
||||
</ScrollViewer>
|
||||
</TabItem>
|
||||
</TabControl>
|
||||
</Grid>
|
||||
</Panel>
|
||||
|
||||
<!-- Console (collapsible) -->
|
||||
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
|
||||
Margin="0,12,0,0" IsVisible="False">
|
||||
<Grid RowDefinitions="Auto,*">
|
||||
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
|
||||
<TextBlock x:Name="ConsoleSectionTitle" Classes="sectionTitle" Text="CONSOLE" VerticalAlignment="Center" />
|
||||
<TextBox Grid.Column="1" FontSize="12" Margin="0,0,12,0" x:Name="ConsoleSearchBox"
|
||||
Watermark="Search..." Width="320" />
|
||||
<CheckBox Grid.Column="2" x:Name="AutoScrollCheck" Content="Auto-scroll" IsChecked="True"
|
||||
FontSize="12" Margin="0,0,12,0" />
|
||||
<Button Grid.Column="3" x:Name="DetachConsoleButton" Classes="ghost" Content="Split" FontSize="12"
|
||||
Padding="10,4" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="4" x:Name="CopyLogButton" Classes="ghost" Content="Copy" FontSize="12"
|
||||
Padding="10,4" Margin="0,0,8,0" />
|
||||
<Button Grid.Column="5" x:Name="ClearLogButton" Classes="ghost" Content="Clear" FontSize="12"
|
||||
Padding="10,4" />
|
||||
</Grid>
|
||||
<ListBox Grid.Row="1" x:Name="ConsoleList" Classes="console" BorderThickness="0,1,0,0"
|
||||
BorderBrush="{StaticResource CardBorderBrush}" CornerRadius="0,0,12,12">
|
||||
<ListBox.ItemTemplate>
|
||||
<DataTemplate>
|
||||
<TextBlock Text="{Binding Text}" Foreground="{Binding Brush}" TextWrapping="NoWrap" />
|
||||
</DataTemplate>
|
||||
</ListBox.ItemTemplate>
|
||||
</ListBox>
|
||||
</Grid>
|
||||
</Border>
|
||||
|
||||
<!-- Launch bar; capped so it does not sprawl on a maximized window. -->
|
||||
<Border Grid.Row="3" Classes="card" Margin="0,12,0,0" Padding="14" MaxWidth="1280">
|
||||
<StackPanel Spacing="14">
|
||||
<Grid ColumnDefinitions="Auto,*,Auto">
|
||||
|
||||
<!-- Selected game cover thumbnail -->
|
||||
<Border Grid.Column="0" Classes="coverClip" Width="56" Height="56" CornerRadius="8"
|
||||
VerticalAlignment="Center">
|
||||
<Panel x:Name="SelectedCoverPanel">
|
||||
<Border Background="{Binding PlaceholderBrush, FallbackValue={x:Null}}"
|
||||
IsVisible="{Binding !HasCover, FallbackValue=False}">
|
||||
<TextBlock Text="{Binding Initials}" FontSize="20" FontWeight="Bold"
|
||||
Foreground="#E8ECF4" Opacity="0.85"
|
||||
HorizontalAlignment="Center" VerticalAlignment="Center" />
|
||||
</Border>
|
||||
<Image Source="{Binding Cover}" Stretch="UniformToFill"
|
||||
IsVisible="{Binding HasCover, FallbackValue=False}" />
|
||||
</Panel>
|
||||
</Border>
|
||||
|
||||
<StackPanel Grid.Column="1" Spacing="4" VerticalAlignment="Center" Margin="14,0,14,0">
|
||||
<Grid ColumnDefinitions="Auto,Auto,*">
|
||||
<TextBlock Grid.Column="0" x:Name="SelectedGameTitle" Text="No game selected" FontSize="16"
|
||||
FontWeight="Bold" TextTrimming="CharacterEllipsis" VerticalAlignment="Center"
|
||||
MaxWidth="340" Margin="0,0,10,0" />
|
||||
|
||||
<!-- Title id / version / size badges, right next to the
|
||||
title. The title's own MaxWidth (not a "*" column) is
|
||||
what keeps them from drifting to the far right. -->
|
||||
<StackPanel Grid.Column="1" x:Name="SelectedBadgesRow" Orientation="Horizontal" Spacing="6"
|
||||
IsVisible="False" VerticalAlignment="Center">
|
||||
<Border Classes="pill" IsVisible="{Binding HasTitleId, FallbackValue=False}">
|
||||
<TextBlock Text="{Binding TitleId}" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<Border Classes="pill" IsVisible="{Binding HasVersion, FallbackValue=False}">
|
||||
<TextBlock Text="{Binding VersionText}" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
<Border Classes="pill">
|
||||
<TextBlock Text="{Binding SizeText, FallbackValue=''}" FontSize="10" FontWeight="SemiBold"
|
||||
Foreground="{StaticResource MutedBrush}" />
|
||||
</Border>
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
|
||||
<TextBlock x:Name="SelectedGamePath" Text="Pick a game from the library, or open an eboot.bin directly."
|
||||
FontSize="11" Foreground="{StaticResource MutedBrush}" TextTrimming="CharacterEllipsis" />
|
||||
<StackPanel Orientation="Horizontal" Spacing="7">
|
||||
<Ellipse x:Name="StatusDot" Width="8" Height="8" Fill="{StaticResource FaintBrush}"
|
||||
VerticalAlignment="Center" />
|
||||
<TextBlock x:Name="StatusText" Text="Idle" FontSize="11" Foreground="{StaticResource MutedBrush}"
|
||||
VerticalAlignment="Center" />
|
||||
</StackPanel>
|
||||
</StackPanel>
|
||||
|
||||
<StackPanel Grid.Column="2" Orientation="Horizontal" Spacing="8" VerticalAlignment="Center">
|
||||
<ToggleButton x:Name="ConsoleToggle" Classes="ghost" Content="≡ Console" />
|
||||
<Button x:Name="LaunchButton" Classes="accent" Content="▶ Launch" IsEnabled="False" />
|
||||
<Button x:Name="StopButton" Classes="danger" Content="■ Stop" IsEnabled="False" />
|
||||
</StackPanel>
|
||||
</Grid>
|
||||
</StackPanel>
|
||||
</Border>
|
||||
</Grid>
|
||||
|
||||
<!-- Status bar -->
|
||||
<Grid x:Name="StatusBar" Grid.Row="2" Height="32" Background="{StaticResource ChromeBrush}"
|
||||
ColumnDefinitions="*,Auto">
|
||||
<TextBlock x:Name="EmulatorPathText" Grid.Column="0" Text="Emulator: locating…" FontSize="11"
|
||||
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0"
|
||||
TextTrimming="CharacterEllipsis" />
|
||||
<TextBlock x:Name="StatusBarRight" Grid.Column="1" Text="" FontSize="11"
|
||||
Foreground="{StaticResource FaintBrush}" VerticalAlignment="Center" Margin="16,0" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Window>
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,52 @@
|
||||
<!--
|
||||
Copyright (C) 2026 SharpEmu Emulator Project
|
||||
SPDX-License-Identifier: GPL-2.0-or-later
|
||||
-->
|
||||
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<!-- Class library: the GUI is hosted by SharpEmu.CLI, which opens it when
|
||||
the executable is started without arguments. -->
|
||||
<PropertyGroup>
|
||||
<GenerateDocumentationFile>false</GenerateDocumentationFile>
|
||||
<Version>0.0.1</Version>
|
||||
</PropertyGroup>
|
||||
|
||||
<!-- Dependency-free; provides the BuildInfo provenance shown in the
|
||||
title bar. -->
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="Avalonia" />
|
||||
<PackageReference Include="Avalonia.Desktop" />
|
||||
<PackageReference Include="Avalonia.Fonts.Inter" />
|
||||
<PackageReference Include="Avalonia.Themes.Fluent" />
|
||||
<PackageReference Include="Tmds.DBus.Protocol" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<AvaloniaResource Include="..\..\assets\images\SharpEmu.ico" Link="Assets/SharpEmu.ico" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Languages\*.json">
|
||||
<LogicalName>Languages.%(Filename)%(Extension)</LogicalName>
|
||||
</EmbeddedResource>
|
||||
</ItemGroup>
|
||||
|
||||
|
||||
|
||||
<!-- The controller readers (DualSense raw HID + Xbox XInput) are shared
|
||||
with the emulator's pad HLE. They are dependency-free, so they are
|
||||
compiled in directly rather than pulling a reference to all of
|
||||
SharpEmu.Libs into the launcher. -->
|
||||
<ItemGroup>
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\PadState.cs" Link="Input/PadState.cs" />
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\HidNative.cs" Link="Input/HidNative.cs" />
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\DualSenseReader.cs" Link="Input/DualSenseReader.cs" />
|
||||
<Compile Include="..\SharpEmu.Libs\Pad\XInputReader.cs" Link="Input/XInputReader.cs" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,301 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Text;
|
||||
using LibAtrac9;
|
||||
|
||||
namespace SharpEmu.GUI;
|
||||
|
||||
/// <summary>
|
||||
/// Loops a game's sce_sys/snd0.at9 preview music while the game is selected
|
||||
/// in the library, like the console home screen. The ATRAC9 stream is decoded
|
||||
/// to WAV on a background task (vendored LibAtrac9); playback uses winmm's
|
||||
/// PlaySound, so this is Windows-only and a no-op elsewhere.
|
||||
/// </summary>
|
||||
internal sealed class SndPreviewPlayer
|
||||
{
|
||||
private const uint SND_ASYNC = 0x0001;
|
||||
private const uint SND_NODEFAULT = 0x0002;
|
||||
private const uint SND_MEMORY = 0x0004;
|
||||
private const uint SND_LOOP = 0x0008;
|
||||
|
||||
private readonly object _sync = new();
|
||||
private int _generation;
|
||||
private GCHandle _pinnedWav;
|
||||
private bool _playing;
|
||||
private bool _paused;
|
||||
private string? _cachedPath;
|
||||
private byte[]? _cachedWav;
|
||||
|
||||
/// <summary>Starts looping the given snd0.at9 after a short debounce.</summary>
|
||||
public void Play(string at9Path)
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
int generation;
|
||||
lock (_sync)
|
||||
{
|
||||
generation = ++_generation;
|
||||
}
|
||||
|
||||
_ = Task.Run(async () =>
|
||||
{
|
||||
// Debounce so skimming through the library does not decode (or
|
||||
// start) a preview per tile.
|
||||
await Task.Delay(120).ConfigureAwait(false);
|
||||
|
||||
byte[]? wav;
|
||||
lock (_sync)
|
||||
{
|
||||
if (generation != _generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
wav = string.Equals(_cachedPath, at9Path, StringComparison.OrdinalIgnoreCase)
|
||||
? _cachedWav
|
||||
: null;
|
||||
}
|
||||
|
||||
if (wav is null)
|
||||
{
|
||||
try
|
||||
{
|
||||
wav = DecodeAt9ToWav(File.ReadAllBytes(at9Path));
|
||||
}
|
||||
catch (Exception)
|
||||
{
|
||||
return; // corrupt or unsupported preview: stay silent
|
||||
}
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (generation != _generation)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_cachedPath = at9Path;
|
||||
_cachedWav = wav;
|
||||
StopLocked();
|
||||
|
||||
// The WAV image must stay pinned while winmm plays from it.
|
||||
_pinnedWav = GCHandle.Alloc(wav, GCHandleType.Pinned);
|
||||
_playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT);
|
||||
if (!_playing)
|
||||
{
|
||||
_pinnedWav.Free();
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
public void Stop()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
_generation++;
|
||||
StopLocked();
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Silences playback but keeps the decoded track ready, so
|
||||
/// <see cref="Resume"/> can restart it (winmm cannot truly pause).
|
||||
/// </summary>
|
||||
public void Pause()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_playing)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_ = PlaySound(0, 0, 0);
|
||||
_playing = false;
|
||||
_paused = true;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>Restarts the track silenced by <see cref="Pause"/>.</summary>
|
||||
public void Resume()
|
||||
{
|
||||
if (!OperatingSystem.IsWindows())
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
lock (_sync)
|
||||
{
|
||||
if (!_paused || !_pinnedWav.IsAllocated)
|
||||
{
|
||||
return;
|
||||
}
|
||||
|
||||
_paused = false;
|
||||
_playing = PlaySound(_pinnedWav.AddrOfPinnedObject(), 0, SND_MEMORY | SND_ASYNC | SND_LOOP | SND_NODEFAULT);
|
||||
}
|
||||
}
|
||||
|
||||
private void StopLocked()
|
||||
{
|
||||
_ = PlaySound(0, 0, 0);
|
||||
_playing = false;
|
||||
_paused = false;
|
||||
if (_pinnedWav.IsAllocated)
|
||||
{
|
||||
_pinnedWav.Free();
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly Guid Atrac9SubFormat = new("47E142D2-36BA-4D8D-88FC-61654F8C836C");
|
||||
|
||||
/// <summary>
|
||||
/// Decodes a Sony AT9 (RIFF-wrapped ATRAC9) file to a PCM16 WAV image.
|
||||
/// Layout per Sony's container: an extensible fmt chunk whose extension
|
||||
/// carries the 4-byte codec config, a fact chunk with the sample count
|
||||
/// and encoder delay, and superframes in the data chunk.
|
||||
/// </summary>
|
||||
private static byte[] DecodeAt9ToWav(byte[] file)
|
||||
{
|
||||
using var reader = new BinaryReader(new MemoryStream(file), Encoding.ASCII);
|
||||
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "RIFF")
|
||||
{
|
||||
throw new InvalidDataException("Not a RIFF file.");
|
||||
}
|
||||
|
||||
reader.BaseStream.Position += 4; // riff size
|
||||
if (Encoding.ASCII.GetString(reader.ReadBytes(4)) != "WAVE")
|
||||
{
|
||||
throw new InvalidDataException("Not a WAVE file.");
|
||||
}
|
||||
|
||||
byte[]? configData = null;
|
||||
var sampleCount = 0;
|
||||
var encoderDelay = 0;
|
||||
var dataOffset = -1;
|
||||
var dataSize = 0;
|
||||
|
||||
while (reader.BaseStream.Position + 8 <= reader.BaseStream.Length)
|
||||
{
|
||||
var chunkId = Encoding.ASCII.GetString(reader.ReadBytes(4));
|
||||
var chunkSize = reader.ReadInt32();
|
||||
var chunkStart = reader.BaseStream.Position;
|
||||
switch (chunkId)
|
||||
{
|
||||
case "fmt ":
|
||||
var formatTag = reader.ReadUInt16();
|
||||
reader.BaseStream.Position = chunkStart + 24; // to SubFormat GUID
|
||||
var subFormat = new Guid(reader.ReadBytes(16));
|
||||
if (formatTag != 0xFFFE || subFormat != Atrac9SubFormat)
|
||||
{
|
||||
throw new InvalidDataException("Not an ATRAC9 stream.");
|
||||
}
|
||||
|
||||
reader.BaseStream.Position += 4; // version info
|
||||
configData = reader.ReadBytes(4);
|
||||
break;
|
||||
case "fact":
|
||||
sampleCount = reader.ReadInt32();
|
||||
reader.BaseStream.Position += 4; // input overlap delay
|
||||
encoderDelay = reader.ReadInt32();
|
||||
break;
|
||||
case "data":
|
||||
dataOffset = (int)chunkStart;
|
||||
dataSize = chunkSize;
|
||||
break;
|
||||
}
|
||||
|
||||
reader.BaseStream.Position = chunkStart + chunkSize + (chunkSize & 1);
|
||||
}
|
||||
|
||||
if (configData is null || sampleCount <= 0 || dataOffset < 0)
|
||||
{
|
||||
throw new InvalidDataException("Missing fmt, fact, or data chunk.");
|
||||
}
|
||||
|
||||
var decoder = new Atrac9Decoder();
|
||||
decoder.Initialize(configData);
|
||||
var config = decoder.Config;
|
||||
|
||||
var superframeCount = (sampleCount + encoderDelay + config.SuperframeSamples - 1) / config.SuperframeSamples;
|
||||
superframeCount = Math.Min(superframeCount, dataSize / config.SuperframeBytes);
|
||||
|
||||
var channels = config.ChannelCount;
|
||||
var pcmBuffer = new short[channels][];
|
||||
for (var i = 0; i < channels; i++)
|
||||
{
|
||||
pcmBuffer[i] = new short[config.SuperframeSamples];
|
||||
}
|
||||
|
||||
var wav = new byte[44 + (sampleCount * channels * 2)];
|
||||
WriteWavHeader(wav, channels, config.SampleRate, sampleCount);
|
||||
|
||||
var superframe = new byte[config.SuperframeBytes];
|
||||
var decodedIndex = 0L; // per-channel, includes the encoder delay
|
||||
var written = 0;
|
||||
for (var f = 0; f < superframeCount && written < sampleCount; f++)
|
||||
{
|
||||
Buffer.BlockCopy(file, dataOffset + (f * config.SuperframeBytes), superframe, 0, config.SuperframeBytes);
|
||||
decoder.Decode(superframe, pcmBuffer);
|
||||
for (var s = 0; s < config.SuperframeSamples && written < sampleCount; s++)
|
||||
{
|
||||
if (decodedIndex++ < encoderDelay)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
var sampleOffset = 44 + ((long)written * channels * 2);
|
||||
for (var ch = 0; ch < channels; ch++)
|
||||
{
|
||||
BinaryPrimitives.WriteInt16LittleEndian(
|
||||
wav.AsSpan((int)(sampleOffset + (ch * 2))),
|
||||
pcmBuffer[ch][s]);
|
||||
}
|
||||
|
||||
written++;
|
||||
}
|
||||
}
|
||||
|
||||
return wav;
|
||||
}
|
||||
|
||||
private static void WriteWavHeader(byte[] wav, int channels, int sampleRate, int sampleCount)
|
||||
{
|
||||
var span = wav.AsSpan();
|
||||
Encoding.ASCII.GetBytes("RIFF").CopyTo(span);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(span[4..], wav.Length - 8);
|
||||
Encoding.ASCII.GetBytes("WAVE").CopyTo(span[8..]);
|
||||
Encoding.ASCII.GetBytes("fmt ").CopyTo(span[12..]);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(span[16..], 16);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(span[20..], 1); // PCM
|
||||
BinaryPrimitives.WriteInt16LittleEndian(span[22..], (short)channels);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(span[24..], sampleRate);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(span[28..], sampleRate * channels * 2);
|
||||
BinaryPrimitives.WriteInt16LittleEndian(span[32..], (short)(channels * 2));
|
||||
BinaryPrimitives.WriteInt16LittleEndian(span[34..], 16); // bits per sample
|
||||
Encoding.ASCII.GetBytes("data").CopyTo(span[36..]);
|
||||
BinaryPrimitives.WriteInt32LittleEndian(span[40..], sampleCount * channels * 2);
|
||||
}
|
||||
|
||||
[DllImport("winmm.dll")]
|
||||
[return: MarshalAs(UnmanagedType.Bool)]
|
||||
private static extern bool PlaySound(nint sound, nint module, uint flags);
|
||||
}
|
||||
@@ -0,0 +1,189 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {
|
||||
"Avalonia": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "2C4UxhWUObWGgYKWic1x5BMMWGJP6SElb91WeOxs+X/iR26rtkqpxFFwwo50FXS9AyYnHfk8QKXDEfe7oT/kZA==",
|
||||
"dependencies": {
|
||||
"Avalonia.BuildServices": "11.3.2",
|
||||
"Avalonia.Remote.Protocol": "11.3.18",
|
||||
"MicroCom.Runtime": "0.11.0"
|
||||
}
|
||||
},
|
||||
"Avalonia.Desktop": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "bilMPa5vYiis6fbNovb6esKytBnOCEGojBa1XFegLCRHCP6g6PvZwS0XF/YOAGkENRlHG8dI7lohOpQ9bIkq1g==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Native": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18",
|
||||
"Avalonia.Win32": "11.3.18",
|
||||
"Avalonia.X11": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Fonts.Inter": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "27u6hB3Y2Ue586yjfeVakberY73VNQXtuKwe/P927XG1QPlhsfmOyifLHDDpSHG85Zl1x/Xv9IZ3+tk9FnjcZQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Themes.Fluent": {
|
||||
"type": "Direct",
|
||||
"requested": "[11.3.18, )",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "+Q/TJoynD0zNuu5w2gD+xcTl7GNKJFxlPYAndRLs/mTDrNbbsvv/271WyIysbMPsXSjCyBDp7RCZzQkpD6x5Bg==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Tmds.DBus.Protocol": {
|
||||
"type": "Direct",
|
||||
"requested": "[0.21.3, )",
|
||||
"resolved": "0.21.3",
|
||||
"contentHash": "hDwB8WsQoyALQKqIbwzS68UKdlnafDm4T/DkO/JrA/YIneP/rKv96SxYPVXeh3FP4i/SXfShrYftKLtciJAIlw=="
|
||||
},
|
||||
"Avalonia.Angle.Windows.Natives": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.1.25547.20250602",
|
||||
"contentHash": "ZL0VLc4s9rvNNFt19Pxm5UNAkmKNylugAwJPX9ulXZ6JWs/l6XZihPWWTyezaoNOVyEPU8YbURtW7XMAtqXH5A=="
|
||||
},
|
||||
"Avalonia.BuildServices": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.2",
|
||||
"contentHash": "qHDToxto1e3hci5YqbG9n0Ty8mlp3zBUN5wT66wKqaDVzXyQ0do3EnRILd4Ke9jpvsktaPpgE0YjEk7hornryQ=="
|
||||
},
|
||||
"Avalonia.FreeDesktop": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "aUwv8BNruRUOaUfMu4U3uibIUS60/rSHgGOhd8zBkLkpxY3JFJvgRbeq5ZzHIyKXCuKi18PO00YHAgCarp3wdw==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Tmds.DBus.Protocol": "0.21.3"
|
||||
}
|
||||
},
|
||||
"Avalonia.Native": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "8g53DROFW6wVJAnTsE1Iu5bdZO5r0oqxbdbMQODs1QDYCK2IU/y/x/vQVKCYOvqxl4tXkzU9tExv8XqSGPWthQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"Avalonia.Remote.Protocol": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "vw+6ZfgTuu72dA9aVWn6u56t2nrBd5MoMU0wo/qI9XJAl/c0oYYphIvwLvJP1JorubQY4UE3d0ac8ULBhrGBiA=="
|
||||
},
|
||||
"Avalonia.Skia": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "/B4aXmNRNjG8I5U/a1xJI+bIi0XO6DDzS3mBrIKlVnJRY2CyZiUeESRQXLnIU77Z9TvqkUROs+D47s085YjFtA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"HarfBuzzSharp": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.Linux": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": "8.3.1.1",
|
||||
"SkiaSharp": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.Linux": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.WebAssembly": "2.88.9"
|
||||
}
|
||||
},
|
||||
"Avalonia.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "eioUHkM2PeLPETd1aEks3rvb9plbba6buIrNdrqCpwE/qgHKUjvRNBd5mUQfAbGgTLiAes524gB8uUMDhrsJVQ==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.Angle.Windows.Natives": "2.1.25547.20250602"
|
||||
}
|
||||
},
|
||||
"Avalonia.X11": {
|
||||
"type": "Transitive",
|
||||
"resolved": "11.3.18",
|
||||
"contentHash": "m4Ki/G5Dovnq+6QzfS0iGbK8V77Q6oTjToMLOB0CxPCCrl3Oxywh6kIjuGJDPaN6kopMmjxlNShyQf+vPYL+JA==",
|
||||
"dependencies": {
|
||||
"Avalonia": "11.3.18",
|
||||
"Avalonia.FreeDesktop": "11.3.18",
|
||||
"Avalonia.Skia": "11.3.18"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "tLZN66oe/uiRPTZfrCU4i8ScVGwqHNh5MHrXj0yVf4l7Mz0FhTGnQ71RGySROTmdognAs0JtluHkL41pIabWuQ==",
|
||||
"dependencies": {
|
||||
"HarfBuzzSharp.NativeAssets.Win32": "8.3.1.1",
|
||||
"HarfBuzzSharp.NativeAssets.macOS": "8.3.1.1"
|
||||
}
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "3EZ1mpIiKWRLL5hUYA82ZHteeDIVaEA/Z0rA/wU6tjx6crcAkJnBPwDXZugBSfo8+J3EznvRJf49uMsqYfKrHg=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "jbtCsgftcaFLCA13tVKo5iWdElJScrulLTKJre36O4YQTIlwDtPPqhRZNk+Y0vv4D1gxbscasGRucUDfS44ofQ=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "loJweK2u/mH/3C2zBa0ggJlITIszOkK64HLAZB7FUT670dTg965whLFYHDQo69NmC4+d9UN0icLC9VHidXaVCA=="
|
||||
},
|
||||
"HarfBuzzSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "8.3.1.1",
|
||||
"contentHash": "UsJtQsfAJoFDZrXc4hCUfRPMqccfKZ0iumJ/upcUjz/cmsTgVFGNEL5yaJWmkqsuFYdMWbj/En5/kS4PFl9hBA=="
|
||||
},
|
||||
"MicroCom.Runtime": {
|
||||
"type": "Transitive",
|
||||
"resolved": "0.11.0",
|
||||
"contentHash": "MEnrZ3UIiH40hjzMDsxrTyi8dtqB5ziv3iBeeU4bXsL/7NLSal9F1lZKpK+tfBRnUoDSdtcW3KufE4yhATOMCA=="
|
||||
},
|
||||
"SkiaSharp": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
|
||||
"dependencies": {
|
||||
"SkiaSharp.NativeAssets.Win32": "2.88.9",
|
||||
"SkiaSharp.NativeAssets.macOS": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Linux": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
|
||||
"dependencies": {
|
||||
"SkiaSharp": "2.88.9"
|
||||
}
|
||||
},
|
||||
"SkiaSharp.NativeAssets.macOS": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.WebAssembly": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
|
||||
},
|
||||
"SkiaSharp.NativeAssets.Win32": {
|
||||
"type": "Transitive",
|
||||
"resolved": "2.88.9",
|
||||
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
|
||||
},
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,11 +3,13 @@
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Linq;
|
||||
using SharpEmu.Logging;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public sealed class Aerolib : ISymbolCatalog
|
||||
{
|
||||
private static readonly SharpEmuLogger Log = SharpEmuLog.For("Aerolib");
|
||||
private static readonly Lazy<Aerolib> _instance = new(() => new Aerolib());
|
||||
private static readonly Aerolib EmptyCatalog = new Aerolib(empty: true);
|
||||
|
||||
@@ -108,14 +110,14 @@ public sealed class Aerolib : ISymbolCatalog
|
||||
|
||||
if (resourceName == null)
|
||||
{
|
||||
Console.Error.WriteLine("[AEROLIB] Embedded resource 'aerolib.bin' not found");
|
||||
Log.Error("Embedded resource 'aerolib.bin' not found");
|
||||
return;
|
||||
}
|
||||
|
||||
using var stream = assembly.GetManifestResourceStream(resourceName);
|
||||
if (stream == null)
|
||||
{
|
||||
Console.Error.WriteLine("[AEROLIB] Failed to open embedded resource stream");
|
||||
Log.Error("Failed to open embedded resource stream");
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -145,11 +147,11 @@ public sealed class Aerolib : ISymbolCatalog
|
||||
_byExportName[name] = symbol;
|
||||
}
|
||||
|
||||
Console.Error.WriteLine($"[AEROLIB] Loaded {_byNid.Count} NID entries from binary resource");
|
||||
Log.Info($"Loaded {_byNid.Count} NID entries from binary resource");
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.Error.WriteLine($"[AEROLIB] Failed to load embedded aerolib.bin: {ex.Message}");
|
||||
Log.Error($"Failed to load embedded aerolib.bin: {ex.Message}", ex);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Binary file not shown.
@@ -2,25 +2,20 @@
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using System.Buffers.Binary;
|
||||
using System.Text;
|
||||
|
||||
namespace SharpEmu.HLE;
|
||||
|
||||
public sealed class CpuContext
|
||||
public sealed class CpuContext(ICpuMemory memory, Generation generation)
|
||||
{
|
||||
private readonly ulong[] _registers = new ulong[16];
|
||||
private readonly ulong[] _xmmRegisters = new ulong[32];
|
||||
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
|
||||
private bool _raxWritten;
|
||||
|
||||
public CpuContext(ICpuMemory memory, Generation generation)
|
||||
{
|
||||
Memory = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||
TargetGeneration = generation;
|
||||
}
|
||||
public ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory));
|
||||
|
||||
public ICpuMemory Memory { get; }
|
||||
|
||||
public Generation TargetGeneration { get; }
|
||||
public Generation TargetGeneration { get; } = generation;
|
||||
|
||||
public ulong Rip { get; set; }
|
||||
|
||||
@@ -130,6 +125,91 @@ public sealed class CpuContext
|
||||
SetYmmUpper(registerIndex, highLow, highHigh);
|
||||
}
|
||||
|
||||
public bool TryReadByte(ulong address, out byte value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[1];
|
||||
if (!Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = buffer[0];
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryReadUInt16(ulong address, out ushort value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(ushort)];
|
||||
if (!Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt16LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteUInt16(ulong address, ushort value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ushort)];
|
||||
BinaryPrimitives.WriteUInt16LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadInt32(ulong address, out int value)
|
||||
{
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
if (!Memory.TryRead(address, bytes))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadInt32LittleEndian(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteInt32(ulong address, int value, bool checkNil = false)
|
||||
{
|
||||
if (checkNil && address == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
Span<byte> bytes = stackalloc byte[sizeof(int)];
|
||||
BinaryPrimitives.WriteInt32LittleEndian(bytes, value);
|
||||
return Memory.TryWrite(address, bytes);
|
||||
}
|
||||
|
||||
public bool TryReadUInt32(ulong address, out uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
if (!Memory.TryRead(address, buffer))
|
||||
{
|
||||
value = 0;
|
||||
return false;
|
||||
}
|
||||
|
||||
value = BinaryPrimitives.ReadUInt32LittleEndian(buffer);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool TryWriteUInt32(ulong address, uint value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(uint)];
|
||||
BinaryPrimitives.WriteUInt32LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryWriteInt64(ulong address, long value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(long)];
|
||||
BinaryPrimitives.WriteInt64LittleEndian(buffer, value);
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadUInt64(ulong address, out ulong value)
|
||||
{
|
||||
Span<byte> buffer = stackalloc byte[sizeof(ulong)];
|
||||
@@ -150,6 +230,33 @@ public sealed class CpuContext
|
||||
return Memory.TryWrite(address, buffer);
|
||||
}
|
||||
|
||||
public bool TryReadNullTerminatedUtf8(ulong address, int capacity, out string value)
|
||||
{
|
||||
value = string.Empty;
|
||||
if (address == 0 || capacity <= 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var bytes = new byte[capacity];
|
||||
for (var index = 0; index < bytes.Length; index++)
|
||||
{
|
||||
if (!Memory.TryRead(address + (ulong)index, bytes.AsSpan(index, 1)))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (bytes[index] == 0)
|
||||
{
|
||||
value = Encoding.UTF8.GetString(bytes, 0, index);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
value = Encoding.UTF8.GetString(bytes);
|
||||
return true;
|
||||
}
|
||||
|
||||
public bool PushUInt64(ulong value)
|
||||
{
|
||||
var rsp = this[CpuRegister.Rsp];
|
||||
@@ -169,4 +276,23 @@ public sealed class CpuContext
|
||||
this[CpuRegister.Rsp] = rsp + sizeof(ulong);
|
||||
return true;
|
||||
}
|
||||
|
||||
public int SetReturn(int result, Type? cast = null)
|
||||
{
|
||||
var value = cast switch
|
||||
{
|
||||
null => (ulong)result,
|
||||
_ when cast == typeof(long) => (ulong)(long)result,
|
||||
_ => throw new NotSupportedException(),
|
||||
};
|
||||
|
||||
this[CpuRegister.Rax] = unchecked(value);
|
||||
return result;
|
||||
}
|
||||
|
||||
public int SetReturn(OrbisGen2Result result)
|
||||
{
|
||||
this[CpuRegister.Rax] = unchecked((ulong)(int)result);
|
||||
return (int)result;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,6 +29,12 @@ public interface IGuestThreadScheduler
|
||||
|
||||
bool TryStartThread(CpuContext creatorContext, GuestThreadStartRequest request, out string? error);
|
||||
|
||||
bool TryJoinThread(
|
||||
CpuContext callerContext,
|
||||
ulong threadHandle,
|
||||
out ulong returnValue,
|
||||
out string? error);
|
||||
|
||||
void Pump(CpuContext callerContext, string reason);
|
||||
|
||||
int WakeBlockedThreads(string wakeKey, int maxCount = int.MaxValue);
|
||||
|
||||
@@ -8,4 +8,6 @@ public interface ICpuMemory
|
||||
bool TryRead(ulong virtualAddress, Span<byte> destination);
|
||||
|
||||
bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source);
|
||||
|
||||
bool TryCompare(ulong virtualAddress, ReadOnlySpan<byte> expected) => false;
|
||||
}
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
|
||||
using System.Collections.Concurrent;
|
||||
using System.Reflection;
|
||||
using System.Runtime.CompilerServices;
|
||||
|
||||
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> _exportNameTable = new(StringComparer.Ordinal);
|
||||
private readonly object _registrationGate = new();
|
||||
private readonly HashSet<(Assembly Assembly, Generation Generation)> _scannedAssemblies = new();
|
||||
private bool _isFrozen;
|
||||
|
||||
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.");
|
||||
}
|
||||
|
||||
// Deduplicated: one assembly is reached through many types.
|
||||
if (!_scannedAssemblies.Add((assembly, generation)))
|
||||
{
|
||||
return 0;
|
||||
}
|
||||
|
||||
var registeredCount = 0;
|
||||
var instances = new Dictionary<Type, object>();
|
||||
|
||||
@@ -47,6 +55,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
var handler = CreateHandler(type, method, instances);
|
||||
if (!_dispatchTable.TryAdd(exportInfo.Value.Nid, handler))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] Duplicate NID '{exportInfo.Value.Nid}' ({exportInfo.Value.ExportName}) — already registered, skipping.");
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -72,8 +81,195 @@ public sealed class ModuleManager : IModuleManager
|
||||
{
|
||||
_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)
|
||||
{
|
||||
ArgumentException.ThrowIfNullOrWhiteSpace(nid);
|
||||
@@ -105,6 +301,7 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if (!_dispatchTable.TryGetValue(nid, out var function) || !_exportTable.TryGetValue(nid, out var export))
|
||||
{
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' not found in dispatch table.");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
return false;
|
||||
@@ -112,11 +309,13 @@ public sealed class ModuleManager : IModuleManager
|
||||
|
||||
if ((export.Target & context.TargetGeneration) == 0)
|
||||
{
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND;
|
||||
Console.Error.WriteLine($"[HLE] NID '{nid}' ({export.Name}) found but not implemented for generation {context.TargetGeneration} (targets: {export.Target}).");
|
||||
context[CpuRegister.Rax] = unchecked((ulong)(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
|
||||
result = OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED;
|
||||
return false;
|
||||
}
|
||||
|
||||
|
||||
context.ClearRaxWriteFlag();
|
||||
int ret = ((SysAbiFunction)function).Invoke(context);
|
||||
|
||||
|
||||
@@ -40,6 +40,13 @@ public enum OrbisGen2Result : int
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_DEADLOCK = unchecked((int)0x8002000B),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the waited-on object was deleted while the caller was
|
||||
/// blocked on it. Matches the SCE kernel EACCES code that waiters of a
|
||||
/// deleted semaphore observe.
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_DELETED = unchecked((int)0x8002000D),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that the target resource is busy.
|
||||
/// </summary>
|
||||
@@ -50,6 +57,13 @@ public enum OrbisGen2Result : int
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_TRY_AGAIN = unchecked((int)0x80020023),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that a blocked wait was canceled (e.g. sceKernelCancelSema).
|
||||
/// Matches the SCE kernel ECANCELED code that canceled semaphore waiters
|
||||
/// observe.
|
||||
/// </summary>
|
||||
ORBIS_GEN2_ERROR_CANCELED = unchecked((int)0x80020055),
|
||||
|
||||
/// <summary>
|
||||
/// Indicates that behavior is recognized but not implemented yet.
|
||||
/// </summary>
|
||||
|
||||
@@ -8,6 +8,14 @@ SPDX-License-Identifier: GPL-2.0-or-later
|
||||
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
|
||||
</PropertyGroup>
|
||||
|
||||
<PropertyGroup>
|
||||
<NoWarn>$(NoWarn);1591</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<EmbeddedResource Include="Aerolib\aerolib.bin" />
|
||||
</ItemGroup>
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
{
|
||||
"version": 2,
|
||||
"dependencies": {
|
||||
"net10.0": {}
|
||||
"net10.0": {
|
||||
"sharpemu.logging": {
|
||||
"type": "Project"
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+3643
-508
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,313 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
internal enum Gen5ShaderEncoding
|
||||
{
|
||||
Sop1,
|
||||
Sop2,
|
||||
Sopc,
|
||||
Sopp,
|
||||
Sopk,
|
||||
Smrd,
|
||||
Smem,
|
||||
Mubuf,
|
||||
Mtbuf,
|
||||
Vop1,
|
||||
Vop2,
|
||||
Vopc,
|
||||
Vop3,
|
||||
Vintrp,
|
||||
Ds,
|
||||
Flat,
|
||||
Vop3p,
|
||||
Mimg,
|
||||
Exp,
|
||||
}
|
||||
|
||||
internal enum Gen5OperandKind
|
||||
{
|
||||
ScalarRegister,
|
||||
VectorRegister,
|
||||
EncodedConstant,
|
||||
LiteralConstant,
|
||||
}
|
||||
|
||||
internal enum Gen5ShaderResourceKind
|
||||
{
|
||||
ReadOnlyTexture,
|
||||
ReadWriteTexture,
|
||||
Sampler,
|
||||
ConstantBuffer,
|
||||
}
|
||||
|
||||
internal enum Gen5PixelOutputKind
|
||||
{
|
||||
Float,
|
||||
Uint,
|
||||
Sint,
|
||||
}
|
||||
|
||||
internal enum Gen5SpirvStage
|
||||
{
|
||||
Vertex,
|
||||
Pixel,
|
||||
Compute,
|
||||
}
|
||||
|
||||
internal sealed record Gen5SpirvShader(
|
||||
byte[] Spirv,
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
||||
uint AttributeCount,
|
||||
IReadOnlyList<Gen5VertexInputBinding> VertexInputs);
|
||||
|
||||
internal readonly record struct Gen5ShaderResourceMapping(
|
||||
Gen5ShaderResourceKind Kind,
|
||||
uint Slot,
|
||||
uint OffsetDwords,
|
||||
bool SizeFlag);
|
||||
|
||||
internal sealed record Gen5ShaderMetadata(
|
||||
uint ExtendedUserDataSizeDwords,
|
||||
uint ShaderResourceTableSizeDwords,
|
||||
IReadOnlyDictionary<uint, uint> DirectResources,
|
||||
IReadOnlyList<Gen5ShaderResourceMapping> Resources);
|
||||
|
||||
internal readonly record struct Gen5ComputeSystemRegisters(
|
||||
uint? WorkGroupXRegister,
|
||||
uint? WorkGroupYRegister,
|
||||
uint? WorkGroupZRegister,
|
||||
uint? ThreadGroupSizeRegister)
|
||||
{
|
||||
public bool TryGetExpression(uint scalarRegister, out string expression)
|
||||
{
|
||||
if (WorkGroupXRegister == scalarRegister)
|
||||
{
|
||||
expression = "gl_WorkGroupID.x";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (WorkGroupYRegister == scalarRegister)
|
||||
{
|
||||
expression = "gl_WorkGroupID.y";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (WorkGroupZRegister == scalarRegister)
|
||||
{
|
||||
expression = "gl_WorkGroupID.z";
|
||||
return true;
|
||||
}
|
||||
|
||||
if (ThreadGroupSizeRegister == scalarRegister)
|
||||
{
|
||||
expression = "(gl_WorkGroupSize.x * gl_WorkGroupSize.y * gl_WorkGroupSize.z)";
|
||||
return true;
|
||||
}
|
||||
|
||||
expression = string.Empty;
|
||||
return false;
|
||||
}
|
||||
|
||||
public void ClearStaticValues(Span<uint> scalarRegisters)
|
||||
{
|
||||
ClearStaticValue(scalarRegisters, WorkGroupXRegister);
|
||||
ClearStaticValue(scalarRegisters, WorkGroupYRegister);
|
||||
ClearStaticValue(scalarRegisters, WorkGroupZRegister);
|
||||
ClearStaticValue(scalarRegisters, ThreadGroupSizeRegister);
|
||||
}
|
||||
|
||||
private static void ClearStaticValue(Span<uint> scalarRegisters, uint? scalarRegister)
|
||||
{
|
||||
if (scalarRegister is { } register && register < scalarRegisters.Length)
|
||||
{
|
||||
scalarRegisters[(int)register] = 0;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal sealed record Gen5ShaderState(
|
||||
Gen5ShaderProgram Program,
|
||||
IReadOnlyList<uint> UserData,
|
||||
Gen5ShaderMetadata? Metadata,
|
||||
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
|
||||
uint UserDataScalarRegisterBase = 0);
|
||||
|
||||
internal readonly record struct Gen5Operand(Gen5OperandKind Kind, uint Value)
|
||||
{
|
||||
public static Gen5Operand Scalar(uint index) =>
|
||||
new(Gen5OperandKind.ScalarRegister, index);
|
||||
|
||||
public static Gen5Operand Vector(uint index) =>
|
||||
new(Gen5OperandKind.VectorRegister, index);
|
||||
|
||||
public static Gen5Operand Source(uint encoded, uint? literal = null)
|
||||
{
|
||||
if (encoded >= 256)
|
||||
{
|
||||
return Vector(encoded - 256);
|
||||
}
|
||||
|
||||
if (encoded is 249 or 255 && literal.HasValue)
|
||||
{
|
||||
return new(Gen5OperandKind.LiteralConstant, literal.Value);
|
||||
}
|
||||
|
||||
if (encoded <= 105 || encoded is 106 or 107 or 124 or 126 or 127)
|
||||
{
|
||||
return Scalar(encoded);
|
||||
}
|
||||
|
||||
return new(Gen5OperandKind.EncodedConstant, encoded);
|
||||
}
|
||||
|
||||
public override string ToString() => Kind switch
|
||||
{
|
||||
Gen5OperandKind.ScalarRegister => $"s{Value}",
|
||||
Gen5OperandKind.VectorRegister => $"v{Value}",
|
||||
Gen5OperandKind.LiteralConstant => $"0x{Value:X8}",
|
||||
_ => $"src[{Value}]",
|
||||
};
|
||||
}
|
||||
|
||||
internal abstract record Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5ImageControl(
|
||||
uint Dmask,
|
||||
uint VectorAddress,
|
||||
IReadOnlyList<uint> AddressRegisters,
|
||||
uint VectorData,
|
||||
uint ScalarResource,
|
||||
uint ScalarSampler,
|
||||
uint Dimension,
|
||||
bool IsArray,
|
||||
bool Glc,
|
||||
bool Slc) : Gen5InstructionControl
|
||||
{
|
||||
public uint GetAddressRegister(int component) =>
|
||||
component < AddressRegisters.Count
|
||||
? AddressRegisters[component]
|
||||
: VectorAddress + (uint)component;
|
||||
}
|
||||
|
||||
internal sealed record Gen5GlobalMemoryControl(
|
||||
uint DwordCount,
|
||||
uint VectorAddress,
|
||||
uint VectorData,
|
||||
uint ScalarAddress,
|
||||
int OffsetBytes,
|
||||
bool Glc,
|
||||
bool Slc) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5BufferMemoryControl(
|
||||
uint DwordCount,
|
||||
uint VectorAddress,
|
||||
uint VectorData,
|
||||
uint ScalarResource,
|
||||
int OffsetBytes,
|
||||
bool IndexEnabled,
|
||||
bool OffsetEnabled,
|
||||
bool Glc,
|
||||
bool Slc) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5ExportControl(
|
||||
uint Target,
|
||||
uint EnableMask,
|
||||
bool Compressed,
|
||||
bool Done,
|
||||
bool ValidMask) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5InterpolationControl(
|
||||
uint Attribute,
|
||||
uint Channel) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5Vop3Control(
|
||||
uint AbsoluteMask,
|
||||
uint NegateMask,
|
||||
uint OutputModifier,
|
||||
bool Clamp,
|
||||
uint? ScalarDestination) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5SdwaControl(
|
||||
uint DestinationSelect,
|
||||
uint Source0Select,
|
||||
uint Source1Select,
|
||||
uint AbsoluteMask,
|
||||
uint NegateMask,
|
||||
uint OutputModifier,
|
||||
bool Clamp) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5DppControl(
|
||||
uint Control,
|
||||
bool FetchInactive,
|
||||
bool BoundControl,
|
||||
uint AbsoluteMask,
|
||||
uint NegateMask,
|
||||
uint BankMask,
|
||||
uint RowMask) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5ScalarMemoryControl(
|
||||
uint DestinationCount,
|
||||
int ImmediateOffsetBytes,
|
||||
uint? DynamicOffsetRegister) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5DataShareControl(
|
||||
uint Offset0,
|
||||
uint Offset1,
|
||||
bool Gds) : Gen5InstructionControl;
|
||||
|
||||
internal sealed record Gen5ImageBinding(
|
||||
uint Pc,
|
||||
string Opcode,
|
||||
Gen5ImageControl Control,
|
||||
IReadOnlyList<uint> ResourceDescriptor,
|
||||
IReadOnlyList<uint> SamplerDescriptor,
|
||||
uint? MipLevel);
|
||||
|
||||
internal sealed record Gen5GlobalMemoryBinding(
|
||||
uint ScalarAddress,
|
||||
ulong BaseAddress,
|
||||
IReadOnlyList<uint> InstructionPcs,
|
||||
byte[] Data);
|
||||
|
||||
internal sealed record Gen5VertexInputBinding(
|
||||
uint Pc,
|
||||
uint Location,
|
||||
uint ComponentCount,
|
||||
uint DataFormat,
|
||||
uint NumberFormat,
|
||||
ulong BaseAddress,
|
||||
uint Stride,
|
||||
uint OffsetBytes,
|
||||
byte[] Data);
|
||||
|
||||
internal sealed record Gen5ShaderEvaluation(
|
||||
IReadOnlyList<uint> InitialScalarRegisters,
|
||||
IReadOnlyList<uint> ScalarRegisters,
|
||||
IReadOnlyDictionary<uint, IReadOnlyList<uint>> ScalarRegistersByPc,
|
||||
IReadOnlyList<Gen5ImageBinding> ImageBindings,
|
||||
IReadOnlyList<Gen5GlobalMemoryBinding> GlobalMemoryBindings,
|
||||
Gen5ComputeSystemRegisters? ComputeSystemRegisters = null,
|
||||
IReadOnlySet<uint>? RuntimeScalarRegisters = null,
|
||||
IReadOnlyList<Gen5VertexInputBinding>? VertexInputs = null);
|
||||
|
||||
internal sealed record Gen5ShaderInstruction(
|
||||
uint Pc,
|
||||
Gen5ShaderEncoding Encoding,
|
||||
string Opcode,
|
||||
IReadOnlyList<uint> Words,
|
||||
IReadOnlyList<Gen5Operand> Sources,
|
||||
IReadOnlyList<Gen5Operand> Destinations,
|
||||
Gen5InstructionControl? Control);
|
||||
|
||||
internal sealed record Gen5ShaderProgram(
|
||||
ulong Address,
|
||||
IReadOnlyList<Gen5ShaderInstruction> Instructions)
|
||||
{
|
||||
public IEnumerable<Gen5ImageControl> ImageResources =>
|
||||
Instructions
|
||||
.Select(instruction => instruction.Control)
|
||||
.OfType<Gen5ImageControl>();
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
using SharpEmu.HLE;
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
internal static class Gen5ShaderMetadataReader
|
||||
{
|
||||
private const ulong ShaderUserDataOffset = 0x08;
|
||||
private const int ResourceClassCount = 4;
|
||||
private const int MaxMetadataEntries = 4096;
|
||||
|
||||
public static bool TryRead(
|
||||
CpuContext ctx,
|
||||
ulong shaderHeaderAddress,
|
||||
out Gen5ShaderMetadata metadata)
|
||||
{
|
||||
metadata = default!;
|
||||
if (!ctx.TryReadUInt64(shaderHeaderAddress + ShaderUserDataOffset, out var userDataAddress) ||
|
||||
userDataAddress == 0 ||
|
||||
!ctx.TryReadUInt64(userDataAddress, out var directResourceOffsetsAddress))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var resourceOffsets = new ulong[ResourceClassCount];
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
if (!ctx.TryReadUInt64(
|
||||
userDataAddress + 0x08 + (ulong)(resourceClass * sizeof(ulong)),
|
||||
out resourceOffsets[resourceClass]))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
if (!ctx.TryReadUInt16(userDataAddress + 0x28, out var extendedUserDataSize) ||
|
||||
!ctx.TryReadUInt16(userDataAddress + 0x2A, out var shaderResourceTableSize) ||
|
||||
!ctx.TryReadUInt16(userDataAddress + 0x2C, out var directResourceCount) ||
|
||||
directResourceCount > MaxMetadataEntries)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var resourceCounts = new ushort[ResourceClassCount];
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
if (!ctx.TryReadUInt16(
|
||||
userDataAddress + 0x2E + (ulong)(resourceClass * sizeof(ushort)),
|
||||
out resourceCounts[resourceClass]) ||
|
||||
resourceCounts[resourceClass] > MaxMetadataEntries)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
var directResources = new Dictionary<uint, uint>();
|
||||
if (directResourceCount != 0)
|
||||
{
|
||||
if (directResourceOffsetsAddress == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint type = 0; type < directResourceCount; type++)
|
||||
{
|
||||
if (!ctx.TryReadUInt16(directResourceOffsetsAddress + type * sizeof(ushort), out var offset))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
if (offset != ushort.MaxValue)
|
||||
{
|
||||
directResources[type] = offset;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var resources = new List<Gen5ShaderResourceMapping>();
|
||||
for (var resourceClass = 0; resourceClass < ResourceClassCount; resourceClass++)
|
||||
{
|
||||
var count = resourceCounts[resourceClass];
|
||||
if (count == 0)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
if (resourceOffsets[resourceClass] == 0)
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
for (uint slot = 0; slot < count; slot++)
|
||||
{
|
||||
if (!ctx.TryReadUInt16(
|
||||
resourceOffsets[resourceClass] + slot * sizeof(ushort),
|
||||
out var sharp))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
var offset = (uint)(sharp & 0x7FFF);
|
||||
if (offset == 0x7FFF)
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
resources.Add(new Gen5ShaderResourceMapping(
|
||||
(Gen5ShaderResourceKind)resourceClass,
|
||||
slot,
|
||||
offset,
|
||||
(sharp & 0x8000) != 0));
|
||||
}
|
||||
}
|
||||
|
||||
metadata = new Gen5ShaderMetadata(
|
||||
extendedUserDataSize,
|
||||
shaderResourceTableSize,
|
||||
directResources,
|
||||
resources);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,102 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
// Holds DCBs whose parsing was suspended on an unsatisfied WAIT_REG_MEM condition.
|
||||
// AgcExports re-checks every waiter against guest memory on each submit and resumes
|
||||
// the ones whose condition became true (labels are advanced by ReleaseMem/WriteData/
|
||||
// DmaData packets or by direct CPU writes).
|
||||
internal static class GpuWaitRegistry
|
||||
{
|
||||
public struct WaitingDcb
|
||||
{
|
||||
public ulong CommandBufferAddress;
|
||||
public ulong ResumeAddress;
|
||||
public uint TotalDwords;
|
||||
public uint ResumeOffset;
|
||||
public ulong WaitAddress;
|
||||
public ulong ReferenceValue;
|
||||
public ulong Mask;
|
||||
public uint CompareFunction;
|
||||
public bool Is64Bit;
|
||||
public object? State;
|
||||
}
|
||||
|
||||
private static readonly object _gate = new();
|
||||
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
|
||||
|
||||
public static void Register(ulong address, WaitingDcb waiter)
|
||||
{
|
||||
waiter.WaitAddress = address;
|
||||
lock (_gate)
|
||||
{
|
||||
if (!_waiters.TryGetValue(address, out var list))
|
||||
{
|
||||
list = new List<WaitingDcb>();
|
||||
_waiters.Add(address, list);
|
||||
}
|
||||
|
||||
list.Add(waiter);
|
||||
}
|
||||
}
|
||||
|
||||
// Re-evaluates every registered waiter. readValue receives (address, is64Bit) and
|
||||
// returns null when the memory is unreadable; such waiters are kept registered.
|
||||
public static List<WaitingDcb>? CollectSatisfied(Func<ulong, bool, ulong?> readValue)
|
||||
{
|
||||
List<WaitingDcb>? woken = null;
|
||||
lock (_gate)
|
||||
{
|
||||
List<ulong>? emptied = null;
|
||||
foreach (var (address, list) in _waiters)
|
||||
{
|
||||
for (var i = list.Count - 1; i >= 0; i--)
|
||||
{
|
||||
var value = readValue(address, list[i].Is64Bit);
|
||||
if (value is null || !Compare(list[i], value.Value))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
woken ??= new List<WaitingDcb>();
|
||||
woken.Add(list[i]);
|
||||
list.RemoveAt(i);
|
||||
}
|
||||
|
||||
if (list.Count == 0)
|
||||
{
|
||||
emptied ??= new List<ulong>();
|
||||
emptied.Add(address);
|
||||
}
|
||||
}
|
||||
|
||||
if (emptied != null)
|
||||
{
|
||||
foreach (var address in emptied)
|
||||
{
|
||||
_waiters.Remove(address);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return woken;
|
||||
}
|
||||
|
||||
public static bool Compare(in WaitingDcb waiter, ulong value)
|
||||
{
|
||||
var masked = value & waiter.Mask;
|
||||
return waiter.CompareFunction switch
|
||||
{
|
||||
1 => masked < waiter.ReferenceValue,
|
||||
2 => masked <= waiter.ReferenceValue,
|
||||
3 => masked == waiter.ReferenceValue,
|
||||
4 => masked != waiter.ReferenceValue,
|
||||
5 => masked >= waiter.ReferenceValue,
|
||||
6 => masked > waiter.ReferenceValue,
|
||||
// 0 is "always" in the PM4 encoding and 7 is reserved; treating both as
|
||||
// satisfied keeps a malformed packet from suspending its DCB forever.
|
||||
_ => true,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
// Copyright (C) 2026 SharpEmu Emulator Project
|
||||
// SPDX-License-Identifier: GPL-2.0-or-later
|
||||
|
||||
namespace SharpEmu.Libs.Agc;
|
||||
|
||||
internal static class SpirvFixedShaders
|
||||
{
|
||||
public static byte[] CreateFullscreenVertex(uint attributeCount)
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
module.AddCapability(SpirvCapability.Shader);
|
||||
|
||||
var voidType = module.TypeVoid();
|
||||
var boolType = module.TypeBool();
|
||||
var uintType = module.TypeInt(32, signed: false);
|
||||
var floatType = module.TypeFloat(32);
|
||||
var vec4Type = module.TypeVector(floatType, 4);
|
||||
var inputUintPointer = module.TypePointer(SpirvStorageClass.Input, uintType);
|
||||
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||
|
||||
var vertexIndex = module.AddGlobalVariable(inputUintPointer, SpirvStorageClass.Input);
|
||||
module.AddName(vertexIndex, "vertexIndex");
|
||||
module.AddDecoration(
|
||||
vertexIndex,
|
||||
SpirvDecoration.BuiltIn,
|
||||
(uint)SpirvBuiltIn.VertexIndex);
|
||||
|
||||
var position = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
|
||||
module.AddName(position, "position");
|
||||
module.AddDecoration(position, SpirvDecoration.BuiltIn, (uint)SpirvBuiltIn.Position);
|
||||
|
||||
var attributes = new uint[attributeCount];
|
||||
for (uint index = 0; index < attributeCount; index++)
|
||||
{
|
||||
attributes[index] =
|
||||
module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
|
||||
module.AddName(attributes[index], $"attr{index}");
|
||||
module.AddDecoration(attributes[index], SpirvDecoration.Location, index);
|
||||
module.AddDecoration(attributes[index], SpirvDecoration.NoPerspective);
|
||||
}
|
||||
|
||||
var functionType = module.TypeFunction(voidType);
|
||||
var main = module.BeginFunction(voidType, functionType);
|
||||
module.AddName(main, "main");
|
||||
module.AddLabel();
|
||||
|
||||
var indexValue = module.AddInstruction(SpirvOp.Load, uintType, vertexIndex);
|
||||
var one = module.Constant(uintType, 1);
|
||||
var two = module.Constant(uintType, 2);
|
||||
var shifted = module.AddInstruction(SpirvOp.ShiftLeftLogical, uintType, indexValue, one);
|
||||
var xBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, shifted, two);
|
||||
var yBits = module.AddInstruction(SpirvOp.BitwiseAnd, uintType, indexValue, two);
|
||||
var x = module.AddInstruction(SpirvOp.ConvertUToF, floatType, xBits);
|
||||
var y = module.AddInstruction(SpirvOp.ConvertUToF, floatType, yBits);
|
||||
var zero = module.ConstantFloat(floatType, 0f);
|
||||
var oneFloat = module.ConstantFloat(floatType, 1f);
|
||||
var twoFloat = module.ConstantFloat(floatType, 2f);
|
||||
var xPosition = module.AddInstruction(SpirvOp.FMul, floatType, x, twoFloat);
|
||||
xPosition = module.AddInstruction(SpirvOp.FSub, floatType, xPosition, oneFloat);
|
||||
var yPosition = module.AddInstruction(SpirvOp.FMul, floatType, y, twoFloat);
|
||||
yPosition = module.AddInstruction(SpirvOp.FSub, floatType, yPosition, oneFloat);
|
||||
var positionValue = module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
vec4Type,
|
||||
xPosition,
|
||||
yPosition,
|
||||
zero,
|
||||
oneFloat);
|
||||
module.AddStatement(SpirvOp.Store, position, positionValue);
|
||||
|
||||
var attributeValue = module.AddInstruction(
|
||||
SpirvOp.CompositeConstruct,
|
||||
vec4Type,
|
||||
x,
|
||||
y,
|
||||
zero,
|
||||
oneFloat);
|
||||
foreach (var attribute in attributes)
|
||||
{
|
||||
module.AddStatement(SpirvOp.Store, attribute, attributeValue);
|
||||
}
|
||||
|
||||
module.AddStatement(SpirvOp.Return);
|
||||
module.EndFunction();
|
||||
|
||||
var interfaces = new uint[2 + attributes.Length];
|
||||
interfaces[0] = vertexIndex;
|
||||
interfaces[1] = position;
|
||||
attributes.CopyTo(interfaces, 2);
|
||||
module.AddEntryPoint(SpirvExecutionModel.Vertex, main, "main", interfaces);
|
||||
_ = boolType;
|
||||
return module.Build();
|
||||
}
|
||||
|
||||
public static byte[] CreateCopyFragment()
|
||||
{
|
||||
var module = new SpirvModuleBuilder();
|
||||
module.AddCapability(SpirvCapability.Shader);
|
||||
|
||||
var voidType = module.TypeVoid();
|
||||
var floatType = module.TypeFloat(32);
|
||||
var vec2Type = module.TypeVector(floatType, 2);
|
||||
var vec4Type = module.TypeVector(floatType, 4);
|
||||
var inputVec4Pointer = module.TypePointer(SpirvStorageClass.Input, vec4Type);
|
||||
var outputVec4Pointer = module.TypePointer(SpirvStorageClass.Output, vec4Type);
|
||||
var imageType = module.TypeImage(
|
||||
floatType,
|
||||
SpirvImageDim.Dim2D,
|
||||
depth: false,
|
||||
arrayed: false,
|
||||
multisampled: false,
|
||||
sampled: 1,
|
||||
SpirvImageFormat.Unknown);
|
||||
var sampledImageType = module.TypeSampledImage(imageType);
|
||||
var sampledImagePointer =
|
||||
module.TypePointer(SpirvStorageClass.UniformConstant, sampledImageType);
|
||||
|
||||
var attribute = module.AddGlobalVariable(inputVec4Pointer, SpirvStorageClass.Input);
|
||||
module.AddName(attribute, "attr0");
|
||||
module.AddDecoration(attribute, SpirvDecoration.Location, 0);
|
||||
|
||||
var texture = module.AddGlobalVariable(
|
||||
sampledImagePointer,
|
||||
SpirvStorageClass.UniformConstant);
|
||||
module.AddName(texture, "tex0");
|
||||
module.AddDecoration(texture, SpirvDecoration.DescriptorSet, 0);
|
||||
module.AddDecoration(texture, SpirvDecoration.Binding, 1);
|
||||
|
||||
var output = module.AddGlobalVariable(outputVec4Pointer, SpirvStorageClass.Output);
|
||||
module.AddName(output, "outColor");
|
||||
module.AddDecoration(output, SpirvDecoration.Location, 0);
|
||||
|
||||
var functionType = module.TypeFunction(voidType);
|
||||
var main = module.BeginFunction(voidType, functionType);
|
||||
module.AddName(main, "main");
|
||||
module.AddLabel();
|
||||
|
||||
var attributeValue = module.AddInstruction(SpirvOp.Load, vec4Type, attribute);
|
||||
var coordinates = module.AddInstruction(
|
||||
SpirvOp.VectorShuffle,
|
||||
vec2Type,
|
||||
attributeValue,
|
||||
attributeValue,
|
||||
0,
|
||||
1);
|
||||
var sampledImage = module.AddInstruction(SpirvOp.Load, sampledImageType, texture);
|
||||
var lod = module.ConstantFloat(floatType, 0f);
|
||||
var color = module.AddInstruction(
|
||||
SpirvOp.ImageSampleExplicitLod,
|
||||
vec4Type,
|
||||
sampledImage,
|
||||
coordinates,
|
||||
2,
|
||||
lod);
|
||||
module.AddStatement(SpirvOp.Store, output, color);
|
||||
module.AddStatement(SpirvOp.Return);
|
||||
module.EndFunction();
|
||||
|
||||
module.AddEntryPoint(
|
||||
SpirvExecutionModel.Fragment,
|
||||
main,
|
||||
"main",
|
||||
[attribute, texture, output]);
|
||||
module.AddExecutionMode(main, SpirvExecutionMode.OriginUpperLeft);
|
||||
return module.Build();
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user