Compare commits

..

3 Commits

Author SHA1 Message Date
ParantezTech 4373f8102e [GUI] set width session menu 2026-07-17 18:09:54 +03:00
ParantezTech 22a38f8849 reuse 2026-07-17 18:09:46 +03:00
ParantezTech 39387fc6e4 [GUI] Add native Vulkan host surface support and more 2026-07-17 17:53:33 +03:00
202 changed files with 1704 additions and 30236 deletions
+11 -18
View File
@@ -1,27 +1,20 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
## Before submitting ## Before submitting
Please read our contribution guidelines before opening a pull request: Please read our contribution guidelines before opening a pull request:
➡️ [**CONTRIBUTING.md**](https://github.com/sharpemu/sharpemu/blob/main/CONTRIBUTING.md) ➡️ [**CONTRIBUTING.md**](CONTRIBUTING.md)
By opening this pull request, you confirm that you have read and agree to follow the contribution guidelines. By opening this pull request, you confirm that you have read and agree to follow the contribution guidelines.
## Testing
If applicable, list the game(s) you tested and briefly describe the results.
Example:
- Demon's Souls (PPSA01341) Boots to splash screen.
- Dreaming Sarah Save/load works correctly.
If your changes do not affect runtime behavior (e.g. documentation, tooling, CI), write `N/A`.
## Checklist ## Checklist
By submitting this PR, you confirm that:
By submitting this pull request, you confirm that: - [ ] I have read `CONTRIBUTING.md`.
- [ ] I tested my changes.
- [ ] I have read and followed `CONTRIBUTING.md`. - [ ] I wrote this description myself and did not copy AI-generated explanations.
- [ ] I tested my changes or marked the testing section as `N/A`. - [ ] I listed the game(s) I tested, if applicable.
- [ ] I wrote this pull request description myself and did not paste AI-generated explanations.
- [ ] I listed the game(s) I tested (or marked the testing section as `N/A`).
-33
View File
@@ -159,11 +159,6 @@ jobs:
NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages NUGET_PACKAGES: ${{ github.workspace }}/.nuget/packages
PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }} PUBLISH_DIR: ${{ github.workspace }}/artifacts/publish/${{ matrix.rid }}
RELEASE_DIR: ${{ github.workspace }}/artifacts/release RELEASE_DIR: ${{ github.workspace }}/artifacts/release
SPIRV_HEADERS_COMMIT: ad9184e76a66b1001c29db9b0a3e87f646c64de0
# SpirvModuleBuilder emits SPIR-V 1.5 and VulkanVideoPresenter requests Vulkan 1.2.
SPIRV_TARGET_ENV: vulkan1.2
SPIRV_TOOLS_COMMIT: 0539c81f69a3daeb706fd3477dca61435b475156
SPIRV_TOOLS_VERSION: v2026.2
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v6 uses: actions/checkout@v6
@@ -188,34 +183,6 @@ jobs:
- name: Run tests - name: Run tests
run: dotnet test SharpEmu.slnx -c Release --no-build --verbosity normal run: dotnet test SharpEmu.slnx -c Release --no-build --verbosity normal
- name: Build pinned SPIRV-Tools
if: matrix.rid == 'linux-x64'
run: |
git clone --no-checkout --filter=blob:none https://github.com/KhronosGroup/SPIRV-Tools.git "$RUNNER_TEMP/spirv-tools"
git -C "$RUNNER_TEMP/spirv-tools" checkout --detach "$SPIRV_TOOLS_COMMIT"
test "$(git -C "$RUNNER_TEMP/spirv-tools" rev-parse HEAD)" = "$SPIRV_TOOLS_COMMIT"
git clone --no-checkout --filter=blob:none https://github.com/KhronosGroup/SPIRV-Headers.git "$RUNNER_TEMP/spirv-tools/external/spirv-headers"
git -C "$RUNNER_TEMP/spirv-tools/external/spirv-headers" checkout --detach "$SPIRV_HEADERS_COMMIT"
test "$(git -C "$RUNNER_TEMP/spirv-tools/external/spirv-headers" rev-parse HEAD)" = "$SPIRV_HEADERS_COMMIT"
cmake -S "$RUNNER_TEMP/spirv-tools" -B "$RUNNER_TEMP/spirv-tools-build" \
-G Ninja \
-DCMAKE_BUILD_TYPE=Release \
-DSPIRV_SKIP_TESTS=ON \
-DSPIRV_WERROR=OFF
cmake --build "$RUNNER_TEMP/spirv-tools-build" --target spirv-val
- name: Generate and validate synthetic SPIR-V
if: matrix.rid == 'linux-x64'
run: |
dotnet run --project tools/SharpEmu.Tools.ShaderDump/SharpEmu.Tools.ShaderDump.csproj -c Release -- artifacts/shader-dump
scripts/validate-synthetic-spirv.sh \
"$RUNNER_TEMP/spirv-tools-build/tools/spirv-val" \
"$SPIRV_TOOLS_VERSION" \
"$SPIRV_TARGET_ENV" \
artifacts/shader-dump
- name: Publish ${{ matrix.rid }} CLI - name: Publish ${{ matrix.rid }} CLI
run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r ${{ matrix.rid }} --self-contained true --no-restore -p:PublishDir="$PUBLISH_DIR" run: dotnet publish src/SharpEmu.CLI/SharpEmu.CLI.csproj -c Release -r ${{ matrix.rid }} --self-contained true --no-restore -p:PublishDir="$PUBLISH_DIR"
-2
View File
@@ -32,8 +32,6 @@ packages/
.nuget/ .nuget/
.dotnet-home/ .dotnet-home/
.cache/ .cache/
__pycache__/
*.py[cod]
.DS_Store .DS_Store
Thumbs.db Thumbs.db
+1 -1
View File
@@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings> <ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable> <Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile> <GenerateDocumentationFile>true</GenerateDocumentationFile>
<SharpEmuVersion>0.0.2-beta.3</SharpEmuVersion> <SharpEmuVersion>0.0.2-beta.2</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version> <Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot> <RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>
+1 -3
View File
@@ -27,9 +27,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
> [!NOTE] > [!NOTE]
> SharpEmu supports Windows x64, Linux x64, and macOS x64. Apple Silicon Macs > SharpEmu supports Windows x64, Linux x64, and macOS x64. Apple Silicon Macs
> can run the macOS x64 build through Rosetta 2, and Windows on ARM devices > can run the macOS x64 build through Rosetta 2.
> (e.g. Snapdragon) can run the Windows x64 build through Windows' built-in
> x64 emulation.
> [!WARNING] > [!WARNING]
> SharpEmu is an experimental PS5 emulator developed from scratch in C#. The current focus is on accuracy and infrastructure setup rather than game-specific compatibility. > SharpEmu is an experimental PS5 emulator developed from scratch in C#. The current focus is on accuracy and infrastructure setup rather than game-specific compatibility.
-3
View File
@@ -8,11 +8,8 @@ path = [
"**/packages.lock.json", "**/packages.lock.json",
"scripts/ps5_names.txt", "scripts/ps5_names.txt",
"src/SharpEmu.GUI/Languages/**", "src/SharpEmu.GUI/Languages/**",
"src/SharpEmu.ShaderCompiler.Metal/Templates/**",
"tests/SharpEmu.ShaderCompiler.Metal.Tests/Goldens/**",
"_logs/**", "_logs/**",
".github/images/**", ".github/images/**",
".github/pull_request_template.md",
"assets/images/**" "assets/images/**"
] ]
precedence = "aggregate" precedence = "aggregate"
-4
View File
@@ -7,20 +7,16 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Folder Name="/src/"> <Folder Name="/src/">
<Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" /> <Project Path="src/SharpEmu.CLI/SharpEmu.CLI.csproj" />
<Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" /> <Project Path="src/SharpEmu.Core/SharpEmu.Core.csproj" />
<Project Path="src/SharpEmu.DebugClient/SharpEmu.DebugClient.csproj" />
<Project Path="src/SharpEmu.Debugger/SharpEmu.Debugger.csproj" />
<Project Path="src/SharpEmu.GUI/SharpEmu.GUI.csproj" /> <Project Path="src/SharpEmu.GUI/SharpEmu.GUI.csproj" />
<Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" /> <Project Path="src/SharpEmu.HLE/SharpEmu.HLE.csproj" />
<Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" /> <Project Path="src/SharpEmu.Libs/SharpEmu.Libs.csproj" />
<Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" /> <Project Path="src/SharpEmu.Logging/SharpEmu.Logging.csproj" />
<Project Path="src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj" /> <Project Path="src/SharpEmu.ShaderCompiler/SharpEmu.ShaderCompiler.csproj" />
<Project Path="src/SharpEmu.ShaderCompiler.Metal/SharpEmu.ShaderCompiler.Metal.csproj" />
<Project Path="src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj" /> <Project Path="src/SharpEmu.ShaderCompiler.Vulkan/SharpEmu.ShaderCompiler.Vulkan.csproj" />
<Project Path="src/SharpEmu.SourceGenerators/SharpEmu.SourceGenerators.csproj" /> <Project Path="src/SharpEmu.SourceGenerators/SharpEmu.SourceGenerators.csproj" />
</Folder> </Folder>
<Folder Name="/tests/"> <Folder Name="/tests/">
<Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" /> <Project Path="tests/SharpEmu.Libs.Tests/SharpEmu.Libs.Tests.csproj" />
<Project Path="tests/SharpEmu.ShaderCompiler.Metal.Tests/SharpEmu.ShaderCompiler.Metal.Tests.csproj" />
<Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" /> <Project Path="tests/SharpEmu.SourceGenerators.Tests/SharpEmu.SourceGenerators.Tests.csproj" />
</Folder> </Folder>
</Solution> </Solution>
Binary file not shown.

Before

Width:  |  Height:  |  Size: 3.9 MiB

+3 -3
View File
@@ -14,9 +14,9 @@ available, presents its decoded BGRA frames at the normal guest-flip boundary.
This preserves the game's own timing and lets the host Vulkan presenter display This preserves the game's own timing and lets the host Vulkan presenter display
the movie without trying to execute the PS5-specific Bink GPU decode path. the movie without trying to execute the PS5-specific Bink GPU decode path.
Without an adapter, Bink files remain visible to the guest and the game's Without an adapter, Bink movies are skipped by default: their open call returns
statically linked decoder runs normally. Set SHARPEMU_BINK_MODE=skip only when not-found so games that mark cinematics as optional progress to their next
explicitly testing a title whose cinematics are optional. state instead of waiting on an empty Bink GPU texture.
Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in, Set SHARPEMU_BINK_MODE=dummy to retain the open and show a built-in,
non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic non-decoded placeholder frame. This requires no SDK, but is a visual diagnostic
-177
View File
@@ -1,177 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Live debug server
SharpEmu can expose a **live debug server** so an external process can inspect
and control a running guest over TCP. The server lives in the emulator; the
companion `SharpEmu.DebugClient` executable is one client, and the wire protocol
is simple enough to script against directly.
This document describes the moving parts and the wire protocol. For day-to-day
client usage, see
[`src/SharpEmu.DebugClient/DEVELOPER_READ.md`](../src/SharpEmu.DebugClient/DEVELOPER_READ.md).
## Layering
| Assembly | Role |
| -------- | ---- |
| `SharpEmu.Core` | Defines the dispatcher seam `ICpuDebugHook` / `ICpuDebugFrame` (namespace `SharpEmu.Core.Cpu.Debug`) and the `CpuExecutionOptions.DebugHook` slot. Core has **no** reference to the debugger. |
| `SharpEmu.Debugger` | The debugger: `DebuggerSession` (implements the hook), `BreakpointStore`, the TCP `DebuggerServer`, the pluggable `IDebugProtocol` with a JSON-lines implementation, and the `DebuggerServerHost` one-call wiring. |
| `SharpEmu.CLI` | Parses `--debug-server`, builds a `DebuggerServerHost`, hands its `Hook` to `SharpEmuRuntimeOptions.DebugHook`, and manages its lifetime. |
| `SharpEmu.DebugClient` | A standalone client executable. Depends only on the BCL. |
The dependency direction is important: Core stays debugger-agnostic and only
publishes the seam. Anything that observes execution implements
`ICpuDebugHook` and is injected through the options, so the debugger can evolve
without touching the CPU core.
## Execution model
`CpuDispatcher` enters a fresh frame for the process entry point and for each
module initializer. When a `DebugHook` is attached it is notified at those
boundaries:
- `OnFrameEnter(frame)` — before the native backend runs the frame. The
`DebuggerSession` decides whether to stop (pause request, breakpoint on the
entry address, single-step, or stop-at-entry). To stop, it **parks the
emulation thread** inside this call on a gate; the frame stays live, so a
client can read and write registers and memory while parked. `continue` /
`step` release the gate.
- `OnFrameExit(frame, result)` — after the frame completes.
Because pausing parks the one thread that owns the guest context, register and
memory accessors are only served while the session reports `Paused`; otherwise
they return "not paused" so a client never observes torn state.
### What is and isn't live yet
- **Live:** attach/handshake, run-state tracking, register read/write, memory
read/write, breakpoint management, execution breakpoints at frame entry,
pause, frame-level step, continue, and stop/resume/terminate events.
- **Surface only (armed as the backend grows hooks):** per-instruction
stepping and data watchpoints (`readwatch` / `writewatch` / `accesswatch`).
The verbs and types exist so clients and tooling can be written now.
## Enabling the server
```bash
SharpEmu --debug-server "/path/to/eboot.bin" # 127.0.0.1:5714
SharpEmu --debug-server=0.0.0.0:5714 "/path/to/eboot.bin"
```
The bind address defaults to loopback; a routable address must be given
explicitly. With stop-at-entry (the default `DebuggerSessionOptions.StopAtEntry`),
the guest parks at its first frame until a client connects and issues
`continue`, giving you a window to set breakpoints before any guest code runs.
## Browser frontend
The dependency-free Python frontend can choose and launch an `eboot.bin`, attach
to its debugger automatically, and provides execution controls, registers,
memory inspection, breakpoint management, process output, and a live protocol
activity stream:
```bash
./tools/SharpEmu.DebuggerFrontend/run.sh
```
It connects to `127.0.0.1:5714` and opens `http://127.0.0.1:8765/` by default.
See [`tools/SharpEmu.DebuggerFrontend/README.md`](../tools/SharpEmu.DebuggerFrontend/README.md)
for configuration and testing options.
## Wire protocol (json-lines/1)
One JSON object per line, UTF-8, `\n`-terminated, in both directions.
### Requests
A `command` string plus command-specific fields. Numeric fields accept a JSON
number or a `0x`-prefixed hex string.
| `command` | Fields | Reply `data` |
| --------- | ------ | ------------ |
| `ping` | — | — |
| `status` (`info`) | — | `state`, `breakpoints`, `lastStop?` |
| `state` | — | `state` |
| `registers` (`regs`) | — | `registers` (rax..r15, rip, rflags, fs_base, gs_base) |
| `set-register` | `register`, `value` | — |
| `read-memory` | `address`, `length` (≤ 65536) | `address`, `length`, `bytes` (hex) |
| `write-memory` | `address`, `bytes` (hex) | `written` |
| `list-breakpoints` (`breakpoints`) | — | `breakpoints[]` |
| `add-breakpoint` (`break`) | `address`, `kind?`, `length?` | `breakpoint` |
| `remove-breakpoint` (`delete-breakpoint`) | `id` | — |
| `enable-breakpoint` | `id`, `enabled?` (default true) | — |
| `continue` (`cont`, `c`) | — | — |
| `step` (`s`) | — | — |
| `pause` | — | — |
### Replies
```json
{"ok":true,"command":"registers","data":{ "registers": { "rax":"0x…", } }}
{"ok":false,"command":"read-memory","error":"Target is not paused."}
```
### Events (unsolicited)
```json
{"event":"hello","protocol":"json-lines/1","state":"Paused"}
{"event":"stopped","reason":"Breakpoint","address":"0x…","frameKind":"ProcessEntry","frameLabel":"eboot.bin","registers":{},"breakpoint":{}}
{"event":"resumed"}
{"event":"terminated"}
```
`reason` is one of `EntryPoint`, `Breakpoint`, `Watchpoint`, `Step`, `Pause`,
`Fault`, or `Stall`.
Stall stops include structured evidence in addition to the human-readable
detail. Import-loop evidence identifies the NID, resolved HLE export, repeating
guest return site, dispatch count, and first two ABI arguments:
```json
{
"event": "stopped",
"reason": "Stall",
"stall": {
"kind": "ImportLoop",
"nid": "9UK1vLZQft4",
"instructionPointer": "0x0000000801CE2418",
"dispatchIndex": 40667904,
"argument0": "0x0000000812345000",
"argument1": "0x0000000000000000",
"resolved": true,
"library": "libKernel",
"function": "scePthreadMutexLock"
}
}
```
The Python frontend uses this evidence to explain the likely failure class and
rank concrete checks/fixes. Its diagnosis is intentionally labelled heuristic:
it helps locate the responsible HLE/scheduler path but does not replace tracing.
## Swapping the protocol
`DebuggerServer` takes an `IDebugProtocol` factory. The default is
`JsonLineDebugProtocol`; a GDB remote serial stub (or any other framing) can be
dropped in without changing the session or command semantics, which live in
`DebugCommandDispatcher`.
## Embedding the server
```csharp
using SharpEmu.Debugger;
using SharpEmu.Core.Runtime;
await using var host = new DebuggerServerHost();
host.Start();
var options = new SharpEmuRuntimeOptions { DebugHook = host.Hook };
using var runtime = SharpEmuRuntime.CreateDefault(options);
var result = runtime.Run(ebootPath);
host.NotifyRunCompleted();
```
+1 -1
View File
@@ -1,6 +1,6 @@
{ {
"sdk": { "sdk": {
"version": "10.0.103", "version": "10.0.103",
"rollForward": "latestFeature" "rollForward": "disable"
} }
} }
+26 -41
View File
@@ -5,62 +5,47 @@
* Build this small adapter with a licensed RAD Bink 2 SDK. The SDK and its * Build this small adapter with a licensed RAD Bink 2 SDK. The SDK and its
* headers are not distributed by SharpEmu. See docs/bink2-bridge.md. * headers are not distributed by SharpEmu. See docs/bink2-bridge.md.
*/ */
#include <stdint.h> #include <stdint.h>
#include "bink.h" #include "bink.h"
typedef struct sharpemu_bink2_info { typedef struct sharpemu_bink2_info {
uint32_t width; uint32_t width;
uint32_t height; uint32_t height;
uint32_t frames_per_second_numerator; uint32_t frames_per_second_numerator;
uint32_t frames_per_second_denominator; uint32_t frames_per_second_denominator;
} sharpemu_bink2_info; } sharpemu_bink2_info;
int sharpemu_bink2_open_utf8(const char *path, HBINK *movie, sharpemu_bink2_info *info) { int sharpemu_bink2_open_utf8(const char *path, HBINK *movie, sharpemu_bink2_info *info) {
HBINK bink; HBINK bink;
if (!path || !movie || !info) return 0; if (!path || !movie || !info) return 0;
*movie = NULL; bink = BinkOpen(path, 0);
if (!bink) return 0;
bink = BinkOpen(path, 0); *movie = bink;
if (!bink) return 0; info->width = bink->Width;
info->height = bink->Height;
if (bink->Width == 0 || bink->Height == 0) { info->frames_per_second_numerator = bink->FrameRate;
BinkClose(bink); info->frames_per_second_denominator = bink->FrameRateDiv;
return 0; return 1;
}
*movie = bink;
info->width = bink->Width;
info->height = bink->Height;
info->frames_per_second_numerator = bink->FrameRate;
info->frames_per_second_denominator = bink->FrameRateDiv;
return 1;
} }
int sharpemu_bink2_decode_next_bgra(HBINK movie, uint8_t *destination, int sharpemu_bink2_decode_next_bgra(HBINK movie, uint8_t *destination,
uint32_t stride, uint32_t destination_bytes) { uint32_t stride, uint32_t destination_bytes) {
uint64_t needed; uint64_t needed;
uint64_t min_stride; if (!movie || !destination || stride < movie->Width * 4) return 0;
needed = (uint64_t)stride * movie->Height;
if (needed > destination_bytes) return 0;
if (!movie || !destination) return 0; /* Async Bink I/O has not filled the next frame yet; retry on the next host present. */
if (BinkWait(movie)) return 0;
min_stride = (uint64_t)movie->Width * 4; BinkDoFrame(movie);
if ((uint64_t)stride < min_stride) return 0; BinkCopyToBuffer(movie, destination, stride, movie->Height, 0, 0, BINKSURFACE32RA);
BinkNextFrame(movie);
needed = (uint64_t)stride * movie->Height; return 1;
if (needed > destination_bytes) return 0;
/* Async Bink I/O has not filled the next frame yet; retry on the next host present. */
if (BinkWait(movie)) return 0;
if (!BinkDoFrame(movie)) return 0;
if (!BinkCopyToBuffer(movie, destination, stride, movie->Height, 0, 0, BINKSURFACE32RA)) return 0;
BinkNextFrame(movie);
return 1;
} }
void sharpemu_bink2_close(HBINK movie) { void sharpemu_bink2_close(HBINK movie) {
if (movie) BinkClose(movie); if (movie) BinkClose(movie);
} }
-55
View File
@@ -1,55 +0,0 @@
#!/usr/bin/env bash
# Copyright (C) 2026 SharpEmu Emulator Project
# SPDX-License-Identifier: GPL-2.0-or-later
set -euo pipefail
if [ "$#" -ne 4 ]; then
echo "usage: $0 <spirv-val> <expected-version> <target-env> <module-directory>" >&2
exit 2
fi
validator=$1
expected_version=$2
target_env=$3
module_directory=$4
if [ ! -x "$validator" ]; then
echo "SPIR-V validator is not executable: $validator" >&2
exit 2
fi
if [ ! -d "$module_directory" ]; then
echo "SPIR-V module directory does not exist: $module_directory" >&2
exit 2
fi
validator_version="$("$validator" --version | head -n 1)"
if [[ "$validator_version" != *"SPIRV-Tools $expected_version"* ]]; then
echo "unexpected SPIRV-Tools version: $validator_version (expected $expected_version)" >&2
exit 2
fi
echo "Validator: $validator_version"
echo "Target environment: $target_env"
mapfile -d '' modules < <(find "$module_directory" -type f -name '*.spv' -print0 | sort -z)
if [ "${#modules[@]}" -eq 0 ]; then
echo "no SPIR-V modules found in $module_directory" >&2
exit 1
fi
failures=0
for module in "${modules[@]}"; do
echo "Validating module: $module"
if ! "$validator" --target-env "$target_env" "$module"; then
echo "SPIR-V validation failed: $module" >&2
failures=1
fi
done
if [ "$failures" -ne 0 ]; then
exit 1
fi
echo "Validated ${#modules[@]} synthetic SPIR-V modules."
+6 -79
View File
@@ -45,6 +45,11 @@ internal static partial class Program
[STAThread] [STAThread]
private static int Main(string[] args) private static int Main(string[] args)
{ {
// Avoid blocking full collections while guest and render threads are
// running, and establish the GC mode before the runtime reserves the
// fixed guest address-space window.
System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.SustainedLowLatency;
try try
{ {
return Run(args); return Run(args);
@@ -257,32 +262,6 @@ internal static partial class Program
return 2; return 2;
} }
if (!TryGetDebugServerOptions(args, out var debugServerEnabled, out var debugServerOptions, out var debugServerError))
{
Log.Error($"Invalid --debug-server endpoint: {debugServerError}");
return 1;
}
SharpEmu.Debugger.DebuggerServerHost? debugHost = null;
if (debugServerEnabled)
{
debugHost = new SharpEmu.Debugger.DebuggerServerHost(debugServerOptions);
try
{
debugHost.Start();
Log.Info($"Live debug server listening on {debugHost.Endpoint}. Attach with SharpEmu.DebugClient.");
// With StopAtEntry, the guest parks at its first frame until a
// client connects and continues.
runtimeOptions = runtimeOptions with { DebugHook = debugHost.Hook };
}
catch (Exception ex)
{
Log.Error("Failed to start the debug server.", ex);
debugHost.DisposeAsync().AsTask().GetAwaiter().GetResult();
return 6;
}
}
Console.Error.WriteLine("[DEBUG] Creating runtime..."); Console.Error.WriteLine("[DEBUG] Creating runtime...");
try try
@@ -356,12 +335,6 @@ internal static partial class Program
} }
finally finally
{ {
if (debugHost is not null)
{
debugHost.NotifyRunCompleted();
debugHost.DisposeAsync().AsTask().GetAwaiter().GetResult();
}
HostSessionControl.SetEmbeddedHostSurface(0); HostSessionControl.SetEmbeddedHostSurface(0);
if (hostSurface is not null) if (hostSurface is not null)
{ {
@@ -1025,45 +998,8 @@ internal static partial class Program
private static void PrintUsage() private static void PrintUsage()
{ {
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] [--log-file[=<path>]] [--debug-server[=host:port]] <path-to-eboot.bin>"); Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--log-level=<level>] [--log-file[=<path>]] <path-to-eboot.bin>");
Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin"""); Log.Info(@"Example: SharpEmu.CLI --cpu-engine=native --trace-imports=64 --log-level=debug --log-file ""E:\Games\...\eboot.bin""");
Log.Info("Debug server: --debug-server starts a live debug listener (default 127.0.0.1:5714); connect with SharpEmu.DebugClient.");
}
/// <summary>
/// Detects the <c>--debug-server</c> flag and parses its optional
/// <c>host:port</c> endpoint. Returns false only when the flag is present but
/// its endpoint is malformed, so the caller can abort with a clear error.
/// </summary>
private static bool TryGetDebugServerOptions(
string[] args,
out bool enabled,
out SharpEmu.Debugger.Server.DebuggerServerOptions options,
out string error)
{
enabled = false;
options = new SharpEmu.Debugger.Server.DebuggerServerOptions();
error = string.Empty;
foreach (var argument in args)
{
if (string.Equals(argument, "--debug-server", StringComparison.OrdinalIgnoreCase))
{
enabled = true;
continue;
}
const string prefix = "--debug-server=";
if (argument.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{
enabled = true;
if (!SharpEmu.Debugger.Server.DebuggerServerOptions.TryParseEndpoint(argument[prefix.Length..], out options, out error))
{
return false;
}
}
}
return true;
} }
private static bool TryParseArguments( private static bool TryParseArguments(
@@ -1097,15 +1033,6 @@ internal static partial class Program
continue; continue;
} }
// The debug-server endpoint is parsed separately (see
// TryGetDebugServerOptions); accept the flag here so it is not
// rejected as an unknown option or mistaken for the eboot path.
if (string.Equals(argument, "--debug-server", StringComparison.OrdinalIgnoreCase) ||
argument.StartsWith("--debug-server=", StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (string.Equals(argument, "--trace-imports", StringComparison.OrdinalIgnoreCase)) if (string.Equals(argument, "--trace-imports", StringComparison.OrdinalIgnoreCase))
{ {
importTraceLimit = DefaultImportTraceLimit; importTraceLimit = DefaultImportTraceLimit;
+1 -1
View File
@@ -7,7 +7,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ItemGroup> <ItemGroup>
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" /> <ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
<ProjectReference Include="..\SharpEmu.Debugger\SharpEmu.Debugger.csproj" />
<ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" /> <ProjectReference Include="..\SharpEmu.GUI\SharpEmu.GUI.csproj" />
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" /> <ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup> </ItemGroup>
@@ -49,6 +48,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<GenerateDocumentationFile>false</GenerateDocumentationFile> <GenerateDocumentationFile>false</GenerateDocumentationFile>
<DebugType>none</DebugType> <DebugType>none</DebugType>
<DebugSymbols>false</DebugSymbols> <DebugSymbols>false</DebugSymbols>
<RestorePackagesWithLockFile>true</RestorePackagesWithLockFile>
</PropertyGroup> </PropertyGroup>
<PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''"> <PropertyGroup Condition="'$(RuntimeIdentifier)' == 'win-x64' Or '$(RuntimeIdentifier)' == ''">
+588
View File
@@ -0,0 +1,588 @@
{
"version": 2,
"dependencies": {
"net10.0": {
"Microsoft.NET.ILLink.Tasks": {
"type": "Direct",
"requested": "[10.0.3, )",
"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",
"contentHash": "jek4XYaQ/PGUwDKKhwR8K47Uh1189PFzMeLqO83mXrXQVIpARZCcfuDedH50YDTepBkfijCZN5U/vZi++erxtg=="
},
"Microsoft.Extensions.DependencyModel": {
"type": "Transitive",
"resolved": "9.0.9",
"contentHash": "fNGvKct2De8ghm0Bpfq0iWthtzIWabgOTi+gJhNOPhNJIowXNEUE2eZNW/zNCzrHVA3PXg2yZ+3cWZndC2IqYA=="
},
"Silk.NET.Core": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "D7AT/nnwlB+4RZ84XY8QNGBZMJI5z9l4CSSETIJ1wCfRJzRt/341y3MRZ4HbnFz4r/IGaWOEZr86iE+0/65yyQ==",
"dependencies": {
"Microsoft.DotNet.PlatformAbstractions": "3.1.6",
"Microsoft.Extensions.DependencyModel": "9.0.9"
}
},
"Silk.NET.GLFW": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "UIs4sH57xlPUNHQ/1bt9rymPWlGy8IMDCNv86h0iM4TOA1CkIx0XM/n/tA4AReh1zQkNrvkxPEdZ3Blvy1dyXg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Ultz.Native.GLFW": "3.4.0"
}
},
"Silk.NET.Input.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "QbJVV7kFBHEByayXCYdJtXXI9Sp4a+QAf0IdGV6uCWkFYcEmqBYW3aaNGvFOdSwTBDbHL5T/OtOCrGh4qYhk7A==",
"dependencies": {
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"Silk.NET.Input.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "KGHYqsv/IQRJtD6dloYh2tN4CkaM40vxM2kj0cGKBoCQiBDYHHhJiyDTyMPx0W7Fz5IgnhnG42ELmIAa0DH69A==",
"dependencies": {
"Silk.NET.Input.Common": "2.23.0",
"Silk.NET.Windowing.Glfw": "2.23.0"
}
},
"Silk.NET.Maths": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "r8PdIVzME8EH0qAgbmRPO87I4GfgR2j8TofT7EMuRJDf1QluoQwnVypDoFJjQ2ZBSRsGYk5unYxxogI05Ogsmw=="
},
"Silk.NET.Windowing.Common": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "ThStSinmY9KQI8DGiF5XEhkLJVnBcgRTBTzL9ijg1wMZAYuckz7ykrNw04fjRm2Gryh6tCNGbvz2XaY0efeFzg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Maths": "2.23.0"
}
},
"Silk.NET.Windowing.Glfw": {
"type": "Transitive",
"resolved": "2.23.0",
"contentHash": "aYBudKmENmvLRn9p15HbdvlQTnnXskcDfTfbYwSb/4fr263rGLwYuDw/txUEc2jihHJiWCp5+75Y7z5wTJWl7g==",
"dependencies": {
"Silk.NET.GLFW": "2.23.0",
"Silk.NET.Windowing.Common": "2.23.0"
}
},
"SkiaSharp": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "3MD5VHjXXieSHCleRLuaTXmL2pD0mB7CcOB1x2kA1I4bhptf4e3R27iM93264ZYuAq6mkUyX5XbcxnZvMJYc1Q==",
"dependencies": {
"SkiaSharp.NativeAssets.Win32": "2.88.9",
"SkiaSharp.NativeAssets.macOS": "2.88.9"
}
},
"SkiaSharp.NativeAssets.Linux": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "cWSaJKVPWAaT/WIn9c8T5uT/l4ETwHxNJTkEOtNKjphNo8AW6TF9O32aRkxqw3l8GUdUo66Bu7EiqtFh/XG0Zg==",
"dependencies": {
"SkiaSharp": "2.88.9"
}
},
"SkiaSharp.NativeAssets.macOS": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "Nv5spmKc4505Ep7oUoJ5vp3KweFpeNqxpyGDWyeEPTX2uR6S6syXIm3gj75dM0YJz7NPvcix48mR5laqs8dPuA=="
},
"SkiaSharp.NativeAssets.WebAssembly": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "kt06RccBHSnAs2wDYdBSfsjIDbY3EpsOVqnlDgKdgvyuRA8ZFDaHRdWNx1VHjGgYzmnFCGiTJBnXFl5BqGwGnA=="
},
"SkiaSharp.NativeAssets.Win32": {
"type": "Transitive",
"resolved": "2.88.9",
"contentHash": "wb2kYgU7iy84nQLYZwMeJXixvK++GoIuECjU4ECaUKNuflyRlJKyiRhN1MAHswvlvzuvkrjRWlK0Za6+kYQK7w=="
},
"Ultz.Native.GLFW": {
"type": "Transitive",
"resolved": "3.4.0",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
},
"sharpemu.core": {
"type": "Project",
"dependencies": {
"Iced": "[1.21.0, )",
"SharpEmu.HLE": "[0.0.2-beta.2, )",
"SharpEmu.Libs": "[0.0.2-beta.2, )",
"SharpEmu.Logging": "[0.0.2-beta.2, )"
}
},
"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.Core": "[0.0.2-beta.2, )",
"SharpEmu.Libs": "[0.0.2-beta.2, )",
"SharpEmu.Logging": "[0.0.2-beta.2, )",
"Tmds.DBus.Protocol": "[0.21.3, )"
}
},
"sharpemu.hle": {
"type": "Project",
"dependencies": {
"SharpEmu.Logging": "[0.0.2-beta.2, )"
}
},
"sharpemu.libs": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[0.0.2-beta.2, )",
"SharpEmu.ShaderCompiler": "[0.0.2-beta.2, )",
"SharpEmu.ShaderCompiler.Vulkan": "[0.0.2-beta.2, )",
"Silk.NET.Input": "[2.23.0, )",
"Silk.NET.Vulkan": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.EXT": "[2.23.0, )",
"Silk.NET.Vulkan.Extensions.KHR": "[2.23.0, )",
"Silk.NET.Windowing": "[2.23.0, )"
}
},
"sharpemu.logging": {
"type": "Project"
},
"sharpemu.shadercompiler": {
"type": "Project",
"dependencies": {
"SharpEmu.HLE": "[0.0.2-beta.2, )"
}
},
"sharpemu.shadercompiler.vulkan": {
"type": "Project",
"dependencies": {
"SharpEmu.ShaderCompiler": "[0.0.2-beta.2, )"
}
},
"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, )",
"resolved": "1.21.0",
"contentHash": "dv5+81Q1TBQvVMSOOOmRcjJmvWcX3BZPZsIq31+RLc5cNft0IHAyNlkdb7ZarOWG913PyBoFDsDXoCIlKmLclg=="
},
"Silk.NET.Input": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "Xzl+tVwAp2eEd8blmGQjmJrsZPnp3PWG0KJjiAQHaY2Zr/ELVWeAROKXmZdCAvexzmte2JVGEy/dxnMycbxlpg==",
"dependencies": {
"Silk.NET.Input.Common": "2.23.0",
"Silk.NET.Input.Glfw": "2.23.0"
}
},
"Silk.NET.Vulkan": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "3/irtlSWXZ3eTi8N6nelI6L34NTB8ZJHpqVMNzZx2aX7Ek9YEQ34NoQW8/Tljrtmkg8KRhHW8hKTEzZaKV8PgA==",
"dependencies": {
"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, )",
"resolved": "2.23.0",
"contentHash": "uRaf4j+SmH3DumjSSSUbFg33BnsGZUyXGj93O9NgGKZSJN3OTmNmQDxRew+/KiVLcgH6qzbto8aNGZ++j9GFWg==",
"dependencies": {
"Silk.NET.Core": "2.23.0",
"Silk.NET.Vulkan": "2.23.0"
}
},
"Silk.NET.Windowing": {
"type": "CentralTransitive",
"requested": "[2.23.0, )",
"resolved": "2.23.0",
"contentHash": "OPNPmt/lRyUKVYrFLQXVxyATqD3MKLc1iY1oKx1/2GppgmZxVZPwN12tekrQ4C7408kgB1L5JD1Wnirqqeb2kg==",
"dependencies": {
"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",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"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",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"net10.0/osx-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",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
},
"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",
"contentHash": "Iy22JopynbOJ32vA0lBhFEzGi65GQJBuJHYBYRBpydrDpNoTiHnjIXfA65Gu+8qsOr/ZEoIF8r9aHCgAXuO6DA=="
}
}
}
}
-20
View File
@@ -3,7 +3,6 @@
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Text; using System.Text;
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.Core.Cpu.Native; using SharpEmu.Core.Cpu.Native;
using SharpEmu.Core.Loader; using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory; using SharpEmu.Core.Memory;
@@ -273,23 +272,7 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
entryFrameDiagnostic, entryFrameDiagnostic,
Environment.NewLine, Environment.NewLine,
"CpuEngine: native-only"); "CpuEngine: native-only");
// Frame boundaries an attached debugger observes; null hook = a branch.
var debugHook = executionOptions.DebugHook;
var debugFrame = debugHook is null
? null
: new CpuContextDebugFrame(
frameKind == EntryFrameKind.ProcessEntry
? CpuDebugFrameKind.ProcessEntry
: CpuDebugFrameKind.ModuleInitializer,
entryPoint,
processImageName,
context,
effectiveImportStubs);
debugHook?.OnFrameEnter(debugFrame!);
_nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager); _nativeCpuBackend ??= new DirectExecutionBackend(_moduleManager);
// Let backend stall reports reference the same frame as entry.
(_nativeCpuBackend as DirectExecutionBackend)?.SetActiveDebugFrame(debugFrame);
if (_nativeCpuBackend.TryExecute( if (_nativeCpuBackend.TryExecute(
context, context,
entryPoint, entryPoint,
@@ -299,7 +282,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
executionOptions, executionOptions,
out var nativeResult)) out var nativeResult))
{ {
debugHook?.OnFrameExit(debugFrame!, nativeResult);
LastSessionSummary = new CpuSessionSummary( LastSessionSummary = new CpuSessionSummary(
nativeResult, nativeResult,
nativeResult == OrbisGen2Result.ORBIS_GEN2_OK nativeResult == OrbisGen2Result.ORBIS_GEN2_OK
@@ -314,8 +296,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
return nativeResult; return nativeResult;
} }
debugHook?.OnFrameExit(debugFrame!, OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
var backendName = string.IsNullOrWhiteSpace(_nativeCpuBackend.BackendName) var backendName = string.IsNullOrWhiteSpace(_nativeCpuBackend.BackendName)
? "native-backend" ? "native-backend"
: _nativeCpuBackend.BackendName; : _nativeCpuBackend.BackendName;
+1 -12
View File
@@ -1,26 +1,15 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Debugging;
namespace SharpEmu.Core.Cpu; namespace SharpEmu.Core.Cpu;
public readonly struct CpuExecutionOptions public readonly struct CpuExecutionOptions
{ {
public bool EnableDisasmDiagnostics { get; init; } public bool EnableDisasmDiagnostics { get; init; }
public CpuExecutionEngine CpuEngine { get; init; } public CpuExecutionEngine CpuEngine { get; init; }
public bool StrictDynlibResolution { get; init; } public bool StrictDynlibResolution { get; init; }
public int ImportTraceLimit { get; init; } public int ImportTraceLimit { get; init; }
/// <summary>
/// An optional debugger attached to this execution session. When set, the
/// dispatcher notifies it at each frame boundary via
/// <see cref="ICpuDebugHook.OnFrameEnter"/> / <see cref="ICpuDebugHook.OnFrameExit"/>.
/// Null when no debugger is attached, which is the default and imposes no
/// runtime cost.
/// </summary>
public ICpuDebugHook? DebugHook { get; init; }
} }
@@ -1,66 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Debugging;
/// <summary>
/// Adapts a live <see cref="CpuContext"/> to <see cref="ICpuDebugFrame"/>. The
/// dispatcher creates one of these around the guest context it is about to run
/// and passes it to the attached <see cref="ICpuDebugHook"/>; every accessor
/// forwards directly to the underlying context.
/// </summary>
internal sealed class CpuContextDebugFrame : ICpuDebugFrame
{
private readonly CpuContext _context;
internal CpuContextDebugFrame(
CpuDebugFrameKind kind,
ulong entryPoint,
string label,
CpuContext context,
IReadOnlyDictionary<ulong, string> importStubs)
{
Kind = kind;
EntryPoint = entryPoint;
Label = label ?? string.Empty;
_context = context ?? throw new ArgumentNullException(nameof(context));
ImportStubs = importStubs ?? new Dictionary<ulong, string>();
}
public CpuDebugFrameKind Kind { get; }
public Generation Generation => _context.TargetGeneration;
public ulong EntryPoint { get; }
public string Label { get; }
public ICpuMemory Memory => _context.Memory;
public ulong GetRegister(CpuRegister register) => _context[register];
public void SetRegister(CpuRegister register, ulong value) => _context[register] = value;
public ulong Rip
{
get => _context.Rip;
set => _context.Rip = value;
}
public ulong Rflags
{
get => _context.Rflags;
set => _context.Rflags = value;
}
public ulong FsBase => _context.FsBase;
public ulong GsBase => _context.GsBase;
public void GetXmm(int registerIndex, out ulong low, out ulong high)
=> _context.GetXmmRegister(registerIndex, out low, out high);
public IReadOnlyDictionary<ulong, string> ImportStubs { get; }
}
@@ -1,18 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Debugging;
/// <summary>
/// Identifies the kind of guest entry frame a debugger is observing. The
/// dispatcher enters a fresh frame for the process entry point and for every
/// module initializer, so the debug layer can label stops accordingly.
/// </summary>
public enum CpuDebugFrameKind
{
/// <summary>The guest process entry point (<c>eboot.bin</c> start).</summary>
ProcessEntry,
/// <summary>A module DT_INIT / initializer routine.</summary>
ModuleInitializer,
}
@@ -1,70 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Core.Cpu.Debugging;
/// <summary>The kind of execution stall the backend detected.</summary>
public enum CpuStallKind
{
/// <summary>
/// The guest is repeatedly re-dispatching the same import with no forward
/// progress — most commonly a spin on a mutex lock/unlock pair.
/// </summary>
ImportLoop,
}
/// <summary>
/// Details of a detected stall handed to <see cref="ICpuDebugHook.OnStall"/>.
/// Reported from the emulation thread at the point the backend recognises the
/// livelock, before it forces the guest out of the loop.
/// </summary>
public readonly struct CpuStallInfo
{
public CpuStallInfo(
CpuStallKind kind,
string? nid,
ulong instructionPointer,
long dispatchIndex,
ulong argument0,
ulong argument1,
string detail,
string? libraryName = null,
string? functionName = null)
{
Kind = kind;
Nid = nid;
InstructionPointer = instructionPointer;
DispatchIndex = dispatchIndex;
Argument0 = argument0;
Argument1 = argument1;
Detail = detail ?? string.Empty;
LibraryName = libraryName;
FunctionName = functionName;
}
public CpuStallKind Kind { get; }
/// <summary>The NID of the import being spun on, when known.</summary>
public string? Nid { get; }
/// <summary>The guest return address of the looping import dispatch.</summary>
public ulong InstructionPointer { get; }
/// <summary>The import dispatch counter at detection time.</summary>
public long DispatchIndex { get; }
/// <summary>The first two guest ABI arguments at stall detection.</summary>
public ulong Argument0 { get; }
public ulong Argument1 { get; }
/// <summary>The resolved HLE export, when the NID is registered.</summary>
public string? LibraryName { get; }
public string? FunctionName { get; }
public bool IsResolved => !string.IsNullOrWhiteSpace(FunctionName);
/// <summary>A human-readable one-line summary of the stall.</summary>
public string Detail { get; }
}
@@ -1,66 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Debugging;
/// <summary>
/// A live view of the guest CPU state at a dispatch boundary, handed to an
/// <see cref="ICpuDebugHook"/> so a debugger can read and mutate registers and
/// guest memory without taking a dependency on the concrete
/// <c>CpuContext</c>/<c>CpuDispatcher</c> types.
/// </summary>
/// <remarks>
/// The frame instance is only valid for the duration of the hook call that
/// receives it (between <see cref="ICpuDebugHook.OnFrameEnter"/> and the
/// matching <see cref="ICpuDebugHook.OnFrameExit"/>). Reads and writes are
/// forwarded straight to the underlying guest context, so mutations made from
/// a hook are observed by the CPU backend when it resumes the frame.
/// </remarks>
public interface ICpuDebugFrame
{
/// <summary>The kind of frame being executed.</summary>
CpuDebugFrameKind Kind { get; }
/// <summary>The guest ABI generation this frame targets.</summary>
Generation Generation { get; }
/// <summary>The guest virtual address the frame begins executing at.</summary>
ulong EntryPoint { get; }
/// <summary>
/// A human-readable label for the frame (process image name or module name).
/// </summary>
string Label { get; }
/// <summary>Guest-addressable memory for this frame.</summary>
ICpuMemory Memory { get; }
/// <summary>Reads a general-purpose register.</summary>
ulong GetRegister(CpuRegister register);
/// <summary>Overwrites a general-purpose register.</summary>
void SetRegister(CpuRegister register, ulong value);
/// <summary>The instruction pointer.</summary>
ulong Rip { get; set; }
/// <summary>The flags register.</summary>
ulong Rflags { get; set; }
/// <summary>The FS segment base (guest TLS pointer).</summary>
ulong FsBase { get; }
/// <summary>The GS segment base.</summary>
ulong GsBase { get; }
/// <summary>Reads the 128-bit value of an XMM register.</summary>
void GetXmm(int registerIndex, out ulong low, out ulong high);
/// <summary>
/// The import stubs (guest address to NID) resolved for this frame, so a
/// debugger can annotate calls into HLE exports.
/// </summary>
IReadOnlyDictionary<ulong, string> ImportStubs { get; }
}
@@ -1,49 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Debugging;
/// <summary>
/// The seam the CPU dispatcher uses to notify an attached debugger when guest
/// execution crosses a frame boundary. Implemented outside of Core (for
/// example by <c>SharpEmu.Debugger</c>) and supplied through
/// <see cref="CpuExecutionOptions.DebugHook"/>.
/// </summary>
/// <remarks>
/// This is intentionally coarse-grained: it exposes the entry and exit of each
/// dispatched frame rather than per-instruction stepping. Per-instruction
/// control requires cooperation from the native execution backend and is layered
/// on top of this seam as the backend gains support; keeping the dispatcher-level
/// contract stable lets the debugger infrastructure exist independently of that
/// work. Implementations must be thread-safe: frames may be dispatched from the
/// dedicated emulation thread while a debug server services clients on its own
/// threads.
/// </remarks>
public interface ICpuDebugHook
{
/// <summary>
/// Invoked immediately before the native backend begins executing a frame.
/// The debugger may inspect or mutate <paramref name="frame"/> and may block
/// the calling thread (for example, to honour a pause request) before
/// returning to allow execution to proceed.
/// </summary>
void OnFrameEnter(ICpuDebugFrame frame);
/// <summary>
/// Invoked after a frame completes, whether it returned to the host or
/// terminated with an error. <paramref name="frame"/> reflects the final
/// guest state.
/// </summary>
void OnFrameExit(ICpuDebugFrame frame, OrbisGen2Result result);
/// <summary>
/// Invoked from the emulation thread when the backend detects an execution
/// stall (for example a mutex spin loop) in the running frame, before it
/// forces the guest out of the loop. As with <see cref="OnFrameEnter"/>, the
/// implementation may inspect <paramref name="frame"/> and block to honour a
/// break before returning to let the backend proceed.
/// </summary>
void OnStall(ICpuDebugFrame frame, CpuStallInfo info);
}
@@ -10,7 +10,6 @@ using System.Runtime.InteropServices;
using System.Security.Cryptography; using System.Security.Cryptography;
using System.Text; using System.Text;
using System.Threading; using System.Threading;
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.Core.Cpu; using SharpEmu.Core.Cpu;
using SharpEmu.HLE; using SharpEmu.HLE;
using SharpEmu.Libs.Kernel; using SharpEmu.Libs.Kernel;
@@ -317,15 +316,11 @@ public sealed partial class DirectExecutionBackend
} }
if (!isGuestWorker && if (!isGuestWorker &&
!ActiveForcedGuestExit && !ActiveForcedGuestExit &&
ShouldForceGuestExitOnImportLoop(in importStubEntry, num7, num, value, value2)) ShouldForceGuestExitOnImportLoop(in importStubEntry, num7, num, value, value2) &&
TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
{ {
// Break before the forced exit so the loop state is still live. cpuContext[CpuRegister.Rax] = 1uL;
NotifyDebuggerStall(CpuStallKind.ImportLoop, in importStubEntry, num7, num, value, value2); return 1uL;
if (TryForceGuestExitToHostStub(argPackPtr, num, num7, importStubEntry.Nid))
{
cpuContext[CpuRegister.Rax] = 1uL;
return 1uL;
}
} }
bool flag0 = importStubEntry.SuppressStrlenTrace; bool flag0 = importStubEntry.SuppressStrlenTrace;
bool flag = num7 >= 2156221920u && num7 <= 2156225024u; bool flag = num7 >= 2156221920u && num7 <= 2156225024u;
@@ -1350,7 +1345,8 @@ public sealed partial class DirectExecutionBackend
out var blockContinuation, out var blockContinuation,
out var hasBlockContinuation, out var hasBlockContinuation,
out var blockWakeKey, out var blockWakeKey,
out var blockWaiter, out var blockResumeHandler,
out var blockWakeHandler,
out var blockDeadlineTimestamp); out var blockDeadlineTimestamp);
if (consumedThreadBlock && if (consumedThreadBlock &&
TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason)) TryYieldGuestThreadToHostStub(argPackPtr, dispatchIndex, returnRip, importStubEntry.Nid, blockReason))
@@ -1361,7 +1357,8 @@ public sealed partial class DirectExecutionBackend
GuestThreadExecution.CurrentGuestThreadHandle, GuestThreadExecution.CurrentGuestThreadHandle,
blockContinuation, blockContinuation,
blockWakeKey, blockWakeKey,
blockWaiter, blockResumeHandler,
blockWakeHandler,
blockDeadlineTimestamp); blockDeadlineTimestamp);
} }
@@ -1410,8 +1407,6 @@ public sealed partial class DirectExecutionBackend
"eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer "eE4Szl8sil8" or // sceKernelAprSubmitCommandBuffer
"qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId "qvMUCyyaCSI" or // sceKernelAprSubmitCommandBufferAndGetId
"Q2V+iqvjgC0" or // vsnprintf "Q2V+iqvjgC0" or // vsnprintf
"AV6ipCNa4Rw" or // strcasecmp
"viiwFMaNamA" or // strstr
"q1cHNfGycLI" or // scePadRead "q1cHNfGycLI" or // scePadRead
"xk0AcarP3V4" or // scePadOpen "xk0AcarP3V4" or // scePadOpen
"yH17Q6NWtVg" or // sceUserServiceGetEvent "yH17Q6NWtVg" or // sceUserServiceGetEvent
@@ -1573,8 +1568,6 @@ public sealed partial class DirectExecutionBackend
"WkkeywLJcgU" or // wcslen "WkkeywLJcgU" or // wcslen
"Ovb2dSJOAuE" or // strcmp "Ovb2dSJOAuE" or // strcmp
"aesyjrHVWy4" or // strncmp "aesyjrHVWy4" or // strncmp
"AV6ipCNa4Rw" or // strcasecmp
"viiwFMaNamA" or // strstr
"pNtJdE3x49E" or // wcscmp "pNtJdE3x49E" or // wcscmp
"fV2xHER+bKE" or // wcscoll "fV2xHER+bKE" or // wcscoll
"E8wCoUEbfzk" or // wcsncmp "E8wCoUEbfzk" or // wcsncmp
@@ -9,7 +9,6 @@ using System.Linq;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Threading; using System.Threading;
using SharpEmu.Core.Cpu; using SharpEmu.Core.Cpu;
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.Core.Loader; using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory; using SharpEmu.Core.Memory;
using SharpEmu.HLE; using SharpEmu.HLE;
@@ -236,11 +235,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private CpuContext? _cpuContext; private CpuContext? _cpuContext;
// Debugger seam; both null when no debugger is attached.
private ICpuDebugHook? _debugHook;
private ICpuDebugFrame? _activeDebugFrame;
[ThreadStatic] [ThreadStatic]
private static DirectExecutionBackend? _activeExecutionBackend; private static DirectExecutionBackend? _activeExecutionBackend;
@@ -449,6 +443,10 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
// Stays set through the wake transition; Resume() consumes it when the thread pumps. // Stays set through the wake transition; Resume() consumes it when the thread pumps.
public IGuestThreadBlockWaiter? BlockWaiter { get; set; } public IGuestThreadBlockWaiter? BlockWaiter { get; set; }
public Func<int>? BlockResumeHandler { get; set; }
public Func<bool>? BlockWakeHandler { get; set; }
public long BlockDeadlineTimestamp { get; set; } public long BlockDeadlineTimestamp { get; set; }
public long ImportCount; public long ImportCount;
@@ -896,49 +894,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private bool HasActiveExecutionThread => ReferenceEquals(_activeExecutionBackend, this); private bool HasActiveExecutionThread => ReferenceEquals(_activeExecutionBackend, this);
/// <summary>
/// Binds the debug frame view the dispatcher created for the frame about to
/// run, so stall notifications reference the same frame the debugger saw at
/// entry. Set to null when no debugger is attached.
/// </summary>
internal void SetActiveDebugFrame(ICpuDebugFrame? frame) => _activeDebugFrame = frame;
/// <summary>
/// Notifies an attached debugger of a detected execution stall. No-op when no
/// debugger is attached or no frame is bound. The debugger may block here to
/// present a break before the backend forces the guest out of the loop.
/// </summary>
private void NotifyDebuggerStall(
CpuStallKind kind,
in ImportStubEntry import,
ulong instructionPointer,
long dispatchIndex,
ulong argument0,
ulong argument1)
{
var hook = _debugHook;
var frame = _activeDebugFrame;
if (hook is null || frame is null)
{
return;
}
var export = import.Export;
var exportDescription = export is null ? "unresolved" : $"{export.LibraryName}:{export.Name}";
var detail = $"kind={kind}, nid={import.Nid}, export={exportDescription}, dispatch#{dispatchIndex}, " +
$"rip=0x{instructionPointer:X16}, arg0=0x{argument0:X16}, arg1=0x{argument1:X16}";
hook.OnStall(frame, new CpuStallInfo(
kind,
import.Nid,
instructionPointer,
dispatchIndex,
argument0,
argument1,
detail,
export?.LibraryName,
export?.Name));
}
private CpuContext? ActiveCpuContext => HasActiveExecutionThread ? _activeCpuContext : _cpuContext; private CpuContext? ActiveCpuContext => HasActiveExecutionThread ? _activeCpuContext : _cpuContext;
private ulong ActiveEntryReturnSentinelRip private ulong ActiveEntryReturnSentinelRip
@@ -1100,7 +1055,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
Console.Error.WriteLine(_moduleManager.TryGetExport("L-Q3LEjIbgA", out ExportedFunction export2) ? ("[LOADER][INFO] ExportCheck map_direct: " + export2.LibraryName + ":" + export2.Name) : "[LOADER][INFO] ExportCheck map_direct: MISSING"); Console.Error.WriteLine(_moduleManager.TryGetExport("L-Q3LEjIbgA", out ExportedFunction export2) ? ("[LOADER][INFO] ExportCheck map_direct: " + export2.LibraryName + ":" + export2.Name) : "[LOADER][INFO] ExportCheck map_direct: MISSING");
_entryPoint = entryPoint; _entryPoint = entryPoint;
_cpuContext = context; _cpuContext = context;
_debugHook = executionOptions.DebugHook;
_returnFallbackTarget = context[CpuRegister.Rsi]; _returnFallbackTarget = context[CpuRegister.Rsi];
Volatile.Write(ref _globalFallbackTarget, _returnFallbackTarget); Volatile.Write(ref _globalFallbackTarget, _returnFallbackTarget);
Volatile.Write(ref _globalUnresolvedReturnStub, (ulong)_unresolvedReturnStub); Volatile.Write(ref _globalUnresolvedReturnStub, (ulong)_unresolvedReturnStub);
@@ -1291,12 +1245,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private unsafe bool TryCreateNativeImportIntrinsic(string nid, out nint address) private unsafe bool TryCreateNativeImportIntrinsic(string nid, out nint address)
{ {
if (IsHlePreferredNid(nid))
{
address = 0;
return false;
}
if (nid == "1jfXLRVzisc" && if (nid == "1jfXLRVzisc" &&
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal)) string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_USLEEP"), "1", StringComparison.Ordinal))
{ {
@@ -1408,54 +1356,6 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
0x75, 0xE7, 0x75, 0xE7,
0xC3, 0xC3,
], ],
"AV6ipCNa4Rw" =>
[
0x0F, 0xB6, 0x07,
0x0F, 0xB6, 0x16,
0x8D, 0x48, 0xBF,
0x83, 0xF9, 0x19,
0x77, 0x03,
0x83, 0xC0, 0x20,
0x8D, 0x4A, 0xBF,
0x83, 0xF9, 0x19,
0x77, 0x03,
0x83, 0xC2, 0x20,
0x29, 0xD0,
0x75, 0x0C,
0x85, 0xD2,
0x74, 0x08,
0x48, 0xFF, 0xC7,
0x48, 0xFF, 0xC6,
0xEB, 0xD4,
0xC3,
],
"viiwFMaNamA" =>
[
0x0F, 0xB6, 0x16,
0x84, 0xD2,
0x74, 0x2D,
0x0F, 0xB6, 0x07,
0x84, 0xC0,
0x74, 0x2A,
0x38, 0xD0,
0x75, 0x1D,
0x4C, 0x8D, 0x47, 0x01,
0x4C, 0x8D, 0x4E, 0x01,
0x41, 0x0F, 0xB6, 0x09,
0x84, 0xC9,
0x74, 0x12,
0x41, 0x38, 0x08,
0x75, 0x08,
0x49, 0xFF, 0xC0,
0x49, 0xFF, 0xC1,
0xEB, 0xEB,
0x48, 0xFF, 0xC7,
0xEB, 0xD3,
0x48, 0x89, 0xF8,
0xC3,
0x31, 0xC0,
0xC3,
],
"pNtJdE3x49E" or "fV2xHER+bKE" => "pNtJdE3x49E" or "fV2xHER+bKE" =>
[ [
0x0F, 0xB7, 0x07, 0x0F, 0xB7, 0x07,
@@ -1520,14 +1420,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
"Q3VBxCXhUHs" => "Q3VBxCXhUHs" =>
[ [
0x48, 0x89, 0xF8, 0x48, 0x89, 0xF8,
0x48, 0x85, 0xD2, 0x48, 0x89, 0xD1,
0x74, 0x11, 0xF3, 0xA4,
0x44, 0x8A, 0x06,
0x44, 0x88, 0x07,
0x48, 0xFF, 0xC6,
0x48, 0xFF, 0xC7,
0x48, 0xFF, 0xCA,
0x75, 0xEF,
0xC3, 0xC3,
], ],
"8zTFvBIAIN8" => "8zTFvBIAIN8" =>
@@ -1661,8 +1555,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private static bool IsHlePreferredNid(string nid) private static bool IsHlePreferredNid(string nid)
{ {
return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal) || return string.Equals(nid, "QrZZdJ8XsX0", StringComparison.Ordinal);
string.Equals(nid, "Q3VBxCXhUHs", StringComparison.Ordinal);
} }
private static bool IsLibcLibrary(string libraryName) private static bool IsLibcLibrary(string libraryName)
@@ -2602,22 +2495,28 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
private unsafe bool TryPatchSse4aExtrqBlend(nint address, byte* source) private unsafe bool TryPatchSse4aExtrqBlend(nint address, byte* source)
{ {
// Rosetta does not implement AMD SSE4a EXTRQ. Recognize the compiler's // Rosetta does not implement AMD SSE4a EXTRQ. This exact sequence masks
// EXTRQ+blend idiom (against whichever xmm0-xmm7 it allocated) and rewrite // xmm2 to its low 40 bits, then copies the resulting second dword into
// it into an equivalent SSE4.1 sequence. Match/encode is isolated in // xmm0. PEXTRB/PINSRD provides the same observable result in 12 bytes:
// Sse4aExtrqBlendPatch so it can be unit-tested; here we only patch bytes. // extract source byte 4 and insert the zero-extended value into lane 1.
var window = new ReadOnlySpan<byte>(source, Sse4aExtrqBlendPatch.SequenceLength); ReadOnlySpan<byte> pattern =
if (!Sse4aExtrqBlendPatch.TryMatch(window, out var destRegister, out var srcRegister)) [
0x66, 0x0F, 0x78, 0xC2, 0x28, 0x00,
0xC4, 0xE3, 0x79, 0x02, 0xC2, 0x02,
];
for (var i = 0; i < pattern.Length; i++)
{ {
return false; if (source[i] != pattern[i])
} {
return false;
Span<byte> replacement = stackalloc byte[Sse4aExtrqBlendPatch.SequenceLength]; }
if (!Sse4aExtrqBlendPatch.TryEncode(destRegister, srcRegister, replacement))
{
return false;
} }
ReadOnlySpan<byte> replacement =
[
0x66, 0x0F, 0x3A, 0x14, 0xD0, 0x04,
0x66, 0x0F, 0x3A, 0x22, 0xC0, 0x01,
];
uint oldProtect = 0; uint oldProtect = 0;
if (!VirtualProtect((void*)address, (nuint)replacement.Length, 64u, &oldProtect)) if (!VirtualProtect((void*)address, (nuint)replacement.Length, 64u, &oldProtect))
{ {
@@ -3316,6 +3215,43 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
thread.HasBlockedContinuation = true; thread.HasBlockedContinuation = true;
thread.BlockWakeKey = wakeKey; thread.BlockWakeKey = wakeKey;
thread.BlockWaiter = waiter; thread.BlockWaiter = waiter;
thread.BlockResumeHandler = null;
thread.BlockWakeHandler = null;
thread.BlockDeadlineTimestamp = blockDeadlineTimestamp;
TraceFocusedContinuation(
"register",
guestThreadHandle,
continuation,
wakeKey);
}
}
private void RegisterBlockedGuestThreadContinuation(
ulong guestThreadHandle,
GuestCpuContinuation continuation,
string wakeKey,
Func<int>? resumeHandler,
Func<bool>? wakeHandler,
long blockDeadlineTimestamp)
{
if (guestThreadHandle == 0 || continuation.Rip < 65536 || continuation.Rsp == 0)
{
return;
}
using (LockGate("RegisterBlockedContinuation"))
{
if (!_guestThreads.TryGetValue(guestThreadHandle, out var thread))
{
return;
}
thread.BlockedContinuation = continuation;
thread.HasBlockedContinuation = true;
thread.BlockWakeKey = wakeKey;
thread.BlockWaiter = null;
thread.BlockResumeHandler = resumeHandler;
thread.BlockWakeHandler = wakeHandler;
thread.BlockDeadlineTimestamp = blockDeadlineTimestamp; thread.BlockDeadlineTimestamp = blockDeadlineTimestamp;
TraceFocusedContinuation( TraceFocusedContinuation(
"register", "register",
@@ -3631,10 +3567,11 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
owner.State = GuestThreadRunState.Blocked; owner.State = GuestThreadRunState.Blocked;
owner.BlockReason = callbackReason ?? reason; owner.BlockReason = callbackReason ?? reason;
if (owner.BlockWaiter is not null && owner.BlockWaiter.TryWake()) if (owner.BlockWakeHandler is not null && owner.BlockWakeHandler())
{ {
owner.State = GuestThreadRunState.Ready; owner.State = GuestThreadRunState.Ready;
owner.BlockReason = null; owner.BlockReason = null;
owner.BlockWakeHandler = null;
owner.BlockDeadlineTimestamp = 0; owner.BlockDeadlineTimestamp = 0;
} }
} }
@@ -3646,7 +3583,7 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
} }
GuestCpuContinuation continuation = default; GuestCpuContinuation continuation = default;
IGuestThreadBlockWaiter? blockWaiter = null; Func<int>? resumeHandler = null;
while (!ActiveForcedGuestExit) while (!ActiveForcedGuestExit)
{ {
WakeExpiredBlockedGuestThreads(); WakeExpiredBlockedGuestThreads();
@@ -3667,8 +3604,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
owner.BlockedContinuation = default; owner.BlockedContinuation = default;
owner.HasBlockedContinuation = false; owner.HasBlockedContinuation = false;
owner.BlockWakeKey = null; owner.BlockWakeKey = null;
blockWaiter = owner.BlockWaiter; resumeHandler = owner.BlockResumeHandler;
owner.BlockWaiter = null; owner.BlockResumeHandler = null;
owner.BlockWakeHandler = null;
owner.BlockDeadlineTimestamp = 0; owner.BlockDeadlineTimestamp = 0;
owner.BlockReason = null; owner.BlockReason = null;
owner.State = GuestThreadRunState.Running; owner.State = GuestThreadRunState.Running;
@@ -3691,9 +3629,9 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
return false; return false;
} }
if (blockWaiter is not null) if (resumeHandler is not null)
{ {
continuation = continuation with { Rax = unchecked((ulong)(long)blockWaiter.Resume()) }; continuation = continuation with { Rax = unchecked((ulong)(long)resumeHandler()) };
} }
if (_logGuestThreads) if (_logGuestThreads)
{ {
@@ -3906,7 +3844,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
bool savedHasBlockedContinuation; bool savedHasBlockedContinuation;
GuestCpuContinuation savedBlockedContinuation; GuestCpuContinuation savedBlockedContinuation;
string? savedBlockWakeKey; string? savedBlockWakeKey;
IGuestThreadBlockWaiter? savedBlockWaiter; Func<int>? savedBlockResumeHandler;
Func<bool>? savedBlockWakeHandler;
long savedBlockDeadlineTimestamp; long savedBlockDeadlineTimestamp;
ulong exceptionStackBase; ulong exceptionStackBase;
lock (_guestThreadGate) lock (_guestThreadGate)
@@ -4042,7 +3981,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
savedHasBlockedContinuation = target.HasBlockedContinuation; savedHasBlockedContinuation = target.HasBlockedContinuation;
savedBlockedContinuation = target.BlockedContinuation; savedBlockedContinuation = target.BlockedContinuation;
savedBlockWakeKey = target.BlockWakeKey; savedBlockWakeKey = target.BlockWakeKey;
savedBlockWaiter = target.BlockWaiter; savedBlockResumeHandler = target.BlockResumeHandler;
savedBlockWakeHandler = target.BlockWakeHandler;
savedBlockDeadlineTimestamp = target.BlockDeadlineTimestamp; savedBlockDeadlineTimestamp = target.BlockDeadlineTimestamp;
target.State = GuestThreadRunState.Running; target.State = GuestThreadRunState.Running;
@@ -4052,7 +3992,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
target.HasBlockedContinuation = false; target.HasBlockedContinuation = false;
target.BlockedContinuation = default; target.BlockedContinuation = default;
target.BlockWakeKey = null; target.BlockWakeKey = null;
target.BlockWaiter = null; target.BlockResumeHandler = null;
target.BlockWakeHandler = null;
target.BlockDeadlineTimestamp = 0; target.BlockDeadlineTimestamp = 0;
} }
@@ -4100,7 +4041,8 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
target.HasBlockedContinuation = savedHasBlockedContinuation; target.HasBlockedContinuation = savedHasBlockedContinuation;
target.BlockedContinuation = savedBlockedContinuation; target.BlockedContinuation = savedBlockedContinuation;
target.BlockWakeKey = savedBlockWakeKey; target.BlockWakeKey = savedBlockWakeKey;
target.BlockWaiter = savedBlockWaiter; target.BlockResumeHandler = savedBlockResumeHandler;
target.BlockWakeHandler = savedBlockWakeHandler;
target.BlockDeadlineTimestamp = savedBlockDeadlineTimestamp; target.BlockDeadlineTimestamp = savedBlockDeadlineTimestamp;
// A condition/event wake can arrive while the parked thread is // A condition/event wake can arrive while the parked thread is
@@ -4110,11 +4052,12 @@ public sealed unsafe partial class DirectExecutionBackend : INativeCpuBackend, I
// pthread wait remains parked forever after a GC suspension races it. // pthread wait remains parked forever after a GC suspension races it.
if (target.State == GuestThreadRunState.Blocked && if (target.State == GuestThreadRunState.Blocked &&
target.HasBlockedContinuation && target.HasBlockedContinuation &&
target.BlockWaiter is not null && target.BlockWakeHandler is not null &&
target.BlockWaiter.TryWake()) target.BlockWakeHandler())
{ {
target.State = GuestThreadRunState.Ready; target.State = GuestThreadRunState.Ready;
target.BlockReason = null; target.BlockReason = null;
target.BlockWakeHandler = null;
target.BlockDeadlineTimestamp = 0; target.BlockDeadlineTimestamp = 0;
_readyGuestThreads.Enqueue(target); _readyGuestThreads.Enqueue(target);
Interlocked.Increment(ref _readyGuestThreadCount); Interlocked.Increment(ref _readyGuestThreadCount);
+1 -1
View File
@@ -252,7 +252,7 @@ public static unsafe class JitStubs
var pattern = TlsAccessPattern; var pattern = TlsAccessPattern;
var end = start + length - pattern.Length; var end = start + length - pattern.Length;
for (var ptr = start; ptr <= end; ptr++) for (var ptr = start; ptr < end; ptr++)
{ {
if (MatchesPattern(ptr, pattern)) if (MatchesPattern(ptr, pattern))
{ {
@@ -1,115 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
namespace SharpEmu.Core.Cpu.Native;
/// <summary>
/// Recognizes Sony's AMD-only SSE4a EXTRQ+blend idiom and rewrites it into an
/// equivalent SSE4.1 sequence. SharpEmu executes guest x86-64 natively, but
/// Rosetta 2 and Intel hosts do not implement SSE4a, so the original opcode
/// raises #UD -> SIGILL. The compiler emits the idiom against whichever XMM
/// register it happens to allocate (Dead Cells uses xmm1, others xmm2), so the
/// source register is read from the ModRM r/m field rather than hard-coded.
///
/// The match/encode logic is deliberately free of native page-patching so it
/// can be unit-tested against handcrafted byte sequences.
/// </summary>
public static class Sse4aExtrqBlendPatch
{
/// <summary>Length in bytes of both the matched idiom and its replacement.</summary>
public const int SequenceLength = 12;
/// <summary>
/// Matches the 12-byte idiom, extracting the destination register D and the
/// source (scratch) register N:
/// <code>
/// EXTRQ xmmN, 0x28, 0x00 ; 66 0F 78 /0 28 00 mask xmmN to low 40 bits
/// VPBLENDD xmmD, xmmD, xmmN, 2 ; C4 E3 vvvv 02 /r 02 copy dword 1 into xmmD
/// </code>
/// N lives in the ModRM r/m field of both instructions; D (the blend
/// destination and src1) lives in the VPBLENDD ModRM reg field and VEX.vvvv.
/// Both are xmm0-xmm7 (the VEX byte1 0xE3 pins R/X/B, so no xmm8-15 extension).
/// The compiler allocates whichever registers it likes — Dead Cells builds use
/// D=xmm0 and D=xmm3, others differ — so both are read from the encoding.
/// </summary>
public static bool TryMatch(ReadOnlySpan<byte> source, out int destRegister, out int srcRegister)
{
destRegister = -1;
srcRegister = -1;
if (source.Length < SequenceLength)
{
return false;
}
// EXTRQ xmmN, 0x28, 0x00 : 66 0F 78, ModRM (mod=11 reg=000 rm=N), 28, 00.
if (source[0] != 0x66 || source[1] != 0x0F || source[2] != 0x78 ||
(source[3] & 0xF8) != 0xC0 || source[4] != 0x28 || source[5] != 0x00)
{
return false;
}
var n = source[3] & 0x07;
// VPBLENDD xmmD, xmmD, xmmN, 2 : C4 E3 <W=0 vvvv=~D L=0 pp=01> 02 ModRM 02.
// VEX.byte2 fixed bits (W, L, pp) must read 0b*0000*01; vvvv encodes ~D.
if (source[6] != 0xC4 || source[7] != 0xE3 || (source[8] & 0x87) != 0x01 ||
source[9] != 0x02 || source[11] != 0x02)
{
return false;
}
var d = (~(source[8] >> 3)) & 0x0F;
if (d > 7)
{
return false;
}
// ModRM: mod=11, reg=D (dest = src1), rm=N (src2 = the masked register).
if (source[10] != (0xC0 | (d << 3) | n))
{
return false;
}
destRegister = d;
srcRegister = n;
return true;
}
/// <summary>
/// Writes the SSE4.1 equivalent into <paramref name="destination"/>:
/// <code>
/// PEXTRB eax, xmmN, 4 ; 66 0F 3A 14 /r 04 extract byte 4 (zero-extended)
/// PINSRD xmmD, eax, 1 ; 66 0F 3A 22 /r 01 insert into xmmD dword lane 1
/// </code>
/// After EXTRQ masks xmmN to its low 40 bits, dword 1 is just byte 4
/// zero-extended, so the two-instruction extract/insert reproduces the exact
/// observable result the AMD idiom left in xmmD. eax is a caller-dead scratch
/// at every site the compiler emits this idiom.
/// </summary>
public static bool TryEncode(int destRegister, int srcRegister, Span<byte> destination)
{
if ((uint)destRegister > 7 || (uint)srcRegister > 7 || destination.Length < SequenceLength)
{
return false;
}
// PEXTRB eax, xmmN, 4 : ModRM (mod=11 reg=N rm=000 -> eax), imm8 = byte index 4.
destination[0] = 0x66;
destination[1] = 0x0F;
destination[2] = 0x3A;
destination[3] = 0x14;
destination[4] = (byte)(0xC0 | (srcRegister << 3));
destination[5] = 0x04;
// PINSRD xmmD, eax, 1 : ModRM (mod=11 reg=D -> xmmD, rm=000 -> eax), lane 1.
destination[6] = 0x66;
destination[7] = 0x0F;
destination[8] = 0x3A;
destination[9] = 0x22;
destination[10] = (byte)(0xC0 | (destRegister << 3));
destination[11] = 0x01;
return true;
}
}
+20 -20
View File
@@ -16,9 +16,7 @@ namespace SharpEmu.Core.Loader;
public sealed class SelfLoader : ISelfLoader public sealed class SelfLoader : ISelfLoader
{ {
private const uint ElfMagic = 0x7F454C46; private const uint SelfMagic = 0x4F153D1D;
private const uint Ps4SelfMagic = 0x4F153D1D;
private const uint Ps5SelfMagic = 0x5414F5EE;
private const ulong SelfSegmentFlag = 0x800; private const ulong SelfSegmentFlag = 0x800;
private const int PageSize = 0x1000; private const int PageSize = 0x1000;
private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL; private const ulong ImportStubBaseAddress = 0x0000_7000_0000_0000UL;
@@ -357,11 +355,10 @@ public sealed class SelfLoader : ISelfLoader
throw new InvalidDataException("Input image is too small to contain an ELF header."); throw new InvalidDataException("Input image is too small to contain an ELF header.");
} }
var magic = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]); if (imageData.Length >= sizeof(uint) && BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]) == SelfMagic)
if (magic is Ps4SelfMagic or Ps5SelfMagic)
{ {
var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0); var selfHeader = ReadUnmanaged<SelfHeader>(imageData, 0);
if (!selfHeader.HasKnownLayout) if (!selfHeader.HasKnownLayout || selfHeader.Unknown != 0x22)
{ {
throw new InvalidDataException("SELF header signature is not recognized."); throw new InvalidDataException("SELF header signature is not recognized.");
} }
@@ -384,11 +381,13 @@ public sealed class SelfLoader : ISelfLoader
// acceptable here; anything else — most commonly a still-encrypted // acceptable here; anything else — most commonly a still-encrypted
// retail eboot — must be reported clearly rather than failing later // retail eboot — must be reported clearly rather than failing later
// with an opaque "not a valid ELF header" message. // with an opaque "not a valid ELF header" message.
if (magic != ElfMagic) const uint ElfMagicBigEndian = 0x7F454C46; // "\x7fELF"
var leadingWord = BinaryPrimitives.ReadUInt32BigEndian(imageData[..sizeof(uint)]);
if (leadingWord != ElfMagicBigEndian)
{ {
throw new InvalidDataException( throw new InvalidDataException(
$"Image is neither a decrypted ELF nor a recognized fake-signed SELF " + $"Image is neither a decrypted ELF nor a recognized fake-signed SELF " +
$"(leading bytes 0x{magic:X8}). This is almost certainly a still-encrypted " + $"(leading bytes 0x{leadingWord:X8}). This is almost certainly a still-encrypted " +
$"retail eboot — SharpEmu has no decryption keys and requires a decrypted / " + $"retail eboot — SharpEmu has no decryption keys and requires a decrypted / " +
$"fake-signed (fSELF) image."); $"fake-signed (fSELF) image.");
} }
@@ -2790,27 +2789,28 @@ public sealed class SelfLoader : ISelfLoader
private readonly ushort _size2; private readonly ushort _size2;
private readonly ulong _fileSize; private readonly ulong _fileSize;
private readonly ushort _segmentCount; private readonly ushort _segmentCount;
private readonly ushort _flags; private readonly ushort _unknown;
private readonly uint _padding; private readonly uint _padding;
public ushort SegmentCount => _segmentCount; public ushort SegmentCount => _segmentCount;
public ushort Unknown => _unknown;
public ulong FileSize => _fileSize; public ulong FileSize => _fileSize;
// Version, key type, and flags are signing metadata and vary across
// valid SELF images. They do not change the fixed header layout.
public bool HasKnownLayout => public bool HasKnownLayout =>
((_ident0 == 0x4F && _ident0 == 0x4F &&
_ident1 == 0x15 && _ident1 == 0x15 &&
_ident2 == 0x3D && _ident2 == 0x3D &&
_ident3 == 0x1D) || _ident3 == 0x1D &&
(_ident0 == 0x54 && _ident4 == 0x00 &&
_ident1 == 0x14 &&
_ident2 == 0xF5 &&
_ident3 == 0xEE)) &&
_ident5 == 0x01 && _ident5 == 0x01 &&
_ident6 == 0x01 && _ident6 == 0x01 &&
_ident7 == 0x12; _ident7 == 0x12 &&
_ident8 == 0x01 &&
_ident9 == 0x01 &&
_ident10 == 0x00 &&
_ident11 == 0x00;
} }
[StructLayout(LayoutKind.Sequential, Pack = 1)] [StructLayout(LayoutKind.Sequential, Pack = 1)]
@@ -873,15 +873,6 @@ public sealed unsafe class PhysicalVirtualMemory : IVirtualMemory, IGuestMemoryA
public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source) public bool TryWrite(ulong virtualAddress, ReadOnlySpan<byte> source)
{ {
// A managed write into a page the guest-image write tracker has
// protected surfaces as a fatal AccessViolation — the runtime turns
// SIGSEGV in managed code into an exception before the resumable
// signal bridge can restore access (native guest stores recover
// there). Pre-visit the span so tracked pages are unprotected and
// their owners dirtied before the copy; guest addresses are
// host-identical, matching the tracker's fault addresses.
GuestImageWriteTracker.NotifyManagedWrite(virtualAddress, (ulong)source.Length);
var requiresExclusiveAccess = false; var requiresExclusiveAccess = false;
_gate.EnterReadLock(); _gate.EnterReadLock();
try try
@@ -68,7 +68,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
CpuEngine = cpuExecutionOptions.CpuEngine, CpuEngine = cpuExecutionOptions.CpuEngine,
StrictDynlibResolution = cpuExecutionOptions.StrictDynlibResolution, StrictDynlibResolution = cpuExecutionOptions.StrictDynlibResolution,
ImportTraceLimit = Math.Max(0, cpuExecutionOptions.ImportTraceLimit), ImportTraceLimit = Math.Max(0, cpuExecutionOptions.ImportTraceLimit),
DebugHook = cpuExecutionOptions.DebugHook,
}; };
_fileSystem = fileSystem ?? new PhysicalFileSystem(); _fileSystem = fileSystem ?? new PhysicalFileSystem();
} }
@@ -80,7 +79,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
CpuEngine = options.CpuEngine, CpuEngine = options.CpuEngine,
StrictDynlibResolution = options.StrictDynlibResolution, StrictDynlibResolution = options.StrictDynlibResolution,
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit), ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
DebugHook = options.DebugHook,
}; };
var moduleManager = new ModuleManager(); var moduleManager = new ModuleManager();
// The compile-time generated registry (SharpEmu.SourceGenerators) is the sole // The compile-time generated registry (SharpEmu.SourceGenerators) is the sole
@@ -4,7 +4,6 @@
namespace SharpEmu.Core.Runtime; namespace SharpEmu.Core.Runtime;
using SharpEmu.Core.Cpu; using SharpEmu.Core.Cpu;
using SharpEmu.Core.Cpu.Debugging;
public readonly struct SharpEmuRuntimeOptions public readonly struct SharpEmuRuntimeOptions
{ {
@@ -13,11 +12,4 @@ public readonly struct SharpEmuRuntimeOptions
public bool StrictDynlibResolution { get; init; } public bool StrictDynlibResolution { get; init; }
public int ImportTraceLimit { get; init; } public int ImportTraceLimit { get; init; }
/// <summary>
/// An optional debugger to attach to guest execution. Flows through to
/// <see cref="CpuExecutionOptions.DebugHook"/>. Null (the default) runs with
/// no debugger attached.
/// </summary>
public ICpuDebugHook? DebugHook { get; init; }
} }
@@ -1,54 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Net;
namespace SharpEmu.DebugClient;
/// <summary>
/// Parses the <c>host:port</c> the client connects to. Mirrors the server's
/// defaults (loopback, port 5714) so a bare invocation attaches to a local
/// emulator with no arguments.
/// </summary>
internal static class ClientEndpoint
{
public const int DefaultPort = 5714;
public static bool TryParse(string? text, out string host, out int port, out string error)
{
host = "127.0.0.1";
port = DefaultPort;
error = string.Empty;
if (string.IsNullOrWhiteSpace(text))
{
return true;
}
var value = text.Trim();
var separator = value.LastIndexOf(':');
if (separator >= 0)
{
var portText = value[(separator + 1)..];
if (portText.Length > 0 && (!int.TryParse(portText, out port) || port is <= 0 or > 65535))
{
error = $"Invalid port '{portText}'.";
return false;
}
value = value[..separator];
}
if (!string.IsNullOrWhiteSpace(value))
{
host = string.Equals(value, "localhost", StringComparison.OrdinalIgnoreCase) ? "127.0.0.1" : value;
}
if (!IPAddress.TryParse(host, out _) && !Uri.CheckHostName(host).Equals(UriHostNameType.Dns))
{
error = $"Invalid host '{host}'.";
return false;
}
return true;
}
}
@@ -1,160 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text.Json;
namespace SharpEmu.DebugClient;
/// <summary>
/// Turns a friendly REPL line (<c>mem 0x1000 64</c>) into the JSON request the
/// server understands. Local-only verbs (help, quit) are reported back to the
/// caller instead of producing a request.
/// </summary>
internal static class CommandTranslator
{
public enum ActionKind
{
SendRequest,
ShowHelp,
Quit,
Ignore,
Error,
}
public readonly record struct Result(ActionKind Kind, string? Payload = null, string? Error = null);
public static Result Translate(string line)
{
var trimmed = line.Trim();
if (trimmed.Length == 0)
{
return new Result(ActionKind.Ignore);
}
var parts = trimmed.Split((char[]?)null, StringSplitOptions.RemoveEmptyEntries);
var verb = parts[0].ToLowerInvariant();
switch (verb)
{
case "help" or "?":
return new Result(ActionKind.ShowHelp);
case "quit" or "exit" or "q":
return new Result(ActionKind.Quit);
case "raw":
var json = trimmed[verb.Length..].Trim();
return json.Length == 0
? Error("raw requires a JSON object argument.")
: Send(json);
case "ping":
return Request("ping");
case "status" or "info":
return Request("status");
case "state":
return Request("state");
case "regs" or "registers":
return Request("registers");
case "continue" or "cont" or "c":
return Request("continue");
case "step" or "s":
return Request("step");
case "pause" or "p":
return Request("pause");
case "bp" or "breakpoints" or "bl":
return Request("list-breakpoints");
case "setreg":
return parts.Length >= 3
? Request("set-register", ("register", parts[1]), ("value", parts[2]))
: Error("Usage: setreg <register> <value>");
case "mem" or "read":
return parts.Length >= 3
? Request("read-memory", ("address", parts[1]), ("length", parts[2]))
: Error("Usage: mem <address> <length>");
case "write":
return parts.Length >= 3
? Request("write-memory", ("address", parts[1]), ("bytes", parts[2]))
: Error("Usage: write <address> <hex-bytes>");
case "break" or "b":
if (parts.Length < 2)
{
return Error("Usage: break <address> [kind] [length]");
}
var breakArgs = new List<(string, string)> { ("address", parts[1]) };
if (parts.Length >= 3)
{
breakArgs.Add(("kind", parts[2]));
}
if (parts.Length >= 4)
{
breakArgs.Add(("length", parts[3]));
}
return Request("add-breakpoint", breakArgs.ToArray());
case "del" or "rm" or "delete":
return parts.Length >= 2
? Request("remove-breakpoint", ("id", parts[1]))
: Error("Usage: del <id>");
case "enable":
return parts.Length >= 2
? Request("enable-breakpoint", ("id", parts[1]), ("enabled", "true"))
: Error("Usage: enable <id>");
case "disable":
return parts.Length >= 2
? RequestWithBool("enable-breakpoint", ("id", parts[1]), enabledName: "enabled", enabled: false)
: Error("Usage: disable <id>");
default:
return Error($"Unknown command '{verb}'. Type 'help' for the command list.");
}
}
private static Result Request(string command, params (string Name, string Value)[] args)
{
var payload = new Dictionary<string, object?> { ["command"] = command };
foreach (var (name, value) in args)
{
payload[name] = value;
}
return Send(JsonSerializer.Serialize(payload));
}
private static Result RequestWithBool(string command, (string Name, string Value) idArg, string enabledName, bool enabled)
{
var payload = new Dictionary<string, object?>
{
["command"] = command,
[idArg.Name] = idArg.Value,
[enabledName] = enabled,
};
return Send(JsonSerializer.Serialize(payload));
}
private static Result Send(string json) => new(ActionKind.SendRequest, json);
private static Result Error(string message) => new(ActionKind.Error, Error: message);
public const string HelpText = """
SharpEmu debug client commands:
status | info Show target state and last stop
state Show run state only
regs | registers Dump integer registers (paused only)
setreg <reg> <value> Set a register (rip/rflags/gp, paused only)
mem <addr> <len> Read guest memory as hex (paused only)
write <addr> <hex> Write guest memory from hex (paused only)
break <addr> [kind] [len] Add a breakpoint (kind: execute/readwatch/writewatch/accesswatch)
bp | breakpoints List breakpoints
del <id> Remove a breakpoint
enable <id> / disable <id> Toggle a breakpoint
continue | c Resume the target
step | s Resume and stop at the next frame
pause Ask a running target to stop
ping Round-trip check
raw <json> Send a literal JSON request
help | ? Show this help
quit | exit Disconnect and exit
Addresses and values accept decimal or 0x-prefixed hex.
""";
}
-153
View File
@@ -1,153 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# SharpEmu.DebugClient
A small, standalone command-line client that connects to the SharpEmu
emulator's **live debug server** and drives it interactively. It ships as its
own executable (`SharpEmu.DebugClient`) and takes no dependency on the emulator
assemblies — it speaks the server's line-delimited JSON protocol directly over
TCP, so you can also drive the server from `nc`, a script, or your own tool.
> **Status:** infrastructure. The transport, protocol, session model, and
> breakpoint store are in place. Stops are delivered at **frame boundaries**
> (process entry and each module initializer); per-instruction stepping and data
> watchpoints are part of the surface and become live as the CPU backend grows
> the corresponding hooks. See [`docs/debugger-server.md`](../../docs/debugger-server.md)
> for the architecture and protocol reference.
## How it fits together
```
+-------------------------+ TCP (JSON lines) +----------------------+
| SharpEmu (emulator) | <------------------------------> | SharpEmu.DebugClient |
| --debug-server | | (this executable) |
| | | |
| DebuggerServerHost | | REPL / --exec |
| +- DebuggerServer | frame boundaries via ICpuDebugHook| |
| +- DebuggerSession <-+------ CPU dispatcher ------------ | |
+-------------------------+ +----------------------+
```
The emulator is the **server**; this client is a separate process that connects
to it and issues commands. The two never share memory — everything crosses the
socket as JSON.
## Building
```bash
dotnet build src/SharpEmu.DebugClient/SharpEmu.DebugClient.csproj
```
## Quick start
1. Launch the emulator with the debug server enabled. It listens on
`127.0.0.1:5714` by default and, with stop-at-entry on, parks the guest at
its first frame until you continue:
```bash
SharpEmu --debug-server "/path/to/game/eboot.bin"
# or choose an endpoint:
SharpEmu --debug-server=127.0.0.1:5714 "/path/to/game/eboot.bin"
```
2. In another terminal, attach the client:
```bash
SharpEmu.DebugClient # defaults to 127.0.0.1:5714
SharpEmu.DebugClient 127.0.0.1:5714 # explicit endpoint
```
3. Drive the target:
```
status
regs
break 0x00000008801234a0
continue
mem 0x00000008802000000 64
```
## Invocation
```
SharpEmu.DebugClient [host:port] [--exec "<command>"]... [--quiet]
```
| Option | Meaning |
| ------------- | ------------------------------------------------------------- |
| `host:port` | Server endpoint. Default `127.0.0.1:5714`. `localhost` is fine. |
| `--exec, -e` | Run one command non-interactively, then exit. Repeatable. |
| `--quiet` | Suppress the connection banner. |
| `--help, -h` | Show usage and the command list. |
Non-interactive example (scriptable):
```bash
SharpEmu.DebugClient --exec "break 0x8801234a0" --exec "continue"
```
## Commands
Addresses and values accept decimal or `0x`-prefixed hex. Register and memory
commands only succeed while the target is **paused**.
| Command | Server verb | Description |
| ------- | ----------- | ----------- |
| `status` \| `info` | `status` | Target state plus the last stop. |
| `state` | `state` | Run state only (`Running`/`Paused`/…). |
| `regs` \| `registers` | `registers` | Dump the integer registers. |
| `setreg <reg> <value>` | `set-register` | Set `rip`, `rflags`, or a GP register. |
| `mem <addr> <len>` \| `read <addr> <len>` | `read-memory` | Read guest memory as hex. |
| `write <addr> <hex>` | `write-memory` | Write guest memory from a hex string. |
| `break <addr> [kind] [len]` \| `b …` | `add-breakpoint` | Add a breakpoint. `kind`: `execute` (default), `readwatch`, `writewatch`, `accesswatch`. |
| `bp` \| `breakpoints` | `list-breakpoints` | List breakpoints. |
| `del <id>` \| `rm <id>` | `remove-breakpoint` | Remove a breakpoint. |
| `enable <id>` / `disable <id>` | `enable-breakpoint` | Toggle a breakpoint. |
| `continue` \| `c` | `continue` | Resume a paused target. |
| `step` \| `s` | `step` | Resume and stop at the next frame boundary. |
| `pause` | `pause` | Ask a running target to stop at the next boundary. |
| `ping` | `ping` | Round-trip liveness check. |
| `raw <json>` | *(passthrough)* | Send a literal JSON request. |
| `help` \| `?` | — | Show the command list (local). |
| `quit` \| `exit` | — | Disconnect and exit (local). |
## Output
The client prints two kinds of lines as they arrive:
- `reply>` — the response to a command you sent (`ok`, plus `data` or `error`).
- `event>` — an unsolicited notification: `hello` on connect, `stopped` when the
target hits a breakpoint / entry / step / pause, `resumed` on continue, and
`terminated` when the run ends.
Because replies and events share one stream, the client prints everything it
receives rather than pairing replies to requests — a `stopped` event may arrive
between your command and its reply.
## Protocol (for building your own client)
One JSON object per line, UTF-8, `\n`-terminated, in both directions.
Request:
```json
{"command":"read-memory","address":"0x8802000000","length":64}
```
Reply:
```json
{"ok":true,"command":"read-memory","data":{"address":"0x0000000880200000","length":64,"bytes":"48894C24.."}}
```
Event:
```json
{"event":"stopped","reason":"Breakpoint","address":"0x00000008801234A0","frameKind":"ProcessEntry","frameLabel":"eboot.bin","registers":{ ... }}
```
The full verb list and payload fields live in
[`docs/debugger-server.md`](../../docs/debugger-server.md).
@@ -1,120 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Net.Sockets;
using System.Text;
using System.Text.Json;
namespace SharpEmu.DebugClient;
/// <summary>
/// A thin TCP wrapper around the server's line-delimited JSON protocol: it
/// writes request lines and runs a background loop that prints incoming
/// responses and events as they arrive. Because the stream interleaves replies
/// with asynchronous stop/resume events, a single reader printing everything is
/// simpler and more robust than correlating request/response pairs.
/// </summary>
internal sealed class DebugClientConnection : IAsyncDisposable
{
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
private static readonly JsonSerializerOptions PrettyOptions = new() { WriteIndented = true };
private readonly TcpClient _client;
private readonly StreamReader _reader;
private readonly StreamWriter _writer;
private readonly SemaphoreSlim _writeLock = new(1, 1);
private DebugClientConnection(TcpClient client, NetworkStream stream)
{
_client = client;
_reader = new StreamReader(stream, Utf8NoBom);
_writer = new StreamWriter(stream, Utf8NoBom) { AutoFlush = false };
}
public static async Task<DebugClientConnection> ConnectAsync(string host, int port, CancellationToken cancellationToken)
{
var client = new TcpClient();
await client.ConnectAsync(host, port, cancellationToken).ConfigureAwait(false);
return new DebugClientConnection(client, client.GetStream());
}
/// <summary>Continuously prints incoming lines until the stream closes.</summary>
public async Task ReceiveLoopAsync(CancellationToken cancellationToken)
{
try
{
while (!cancellationToken.IsCancellationRequested)
{
var line = await _reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
if (line is null)
{
Console.WriteLine();
Console.WriteLine("[connection closed by server]");
return;
}
Print(line);
}
}
catch (OperationCanceledException)
{
}
catch (IOException)
{
Console.WriteLine();
Console.WriteLine("[connection lost]");
}
}
public async Task SendAsync(string json, CancellationToken cancellationToken)
{
await _writeLock.WaitAsync(cancellationToken).ConfigureAwait(false);
try
{
await _writer.WriteLineAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false);
await _writer.FlushAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_writeLock.Release();
}
}
private static void Print(string line)
{
try
{
using var document = JsonDocument.Parse(line);
var root = document.RootElement;
var isEvent = root.TryGetProperty("event", out _);
var prefix = isEvent ? "event>" : "reply>";
var pretty = JsonSerializer.Serialize(root, PrettyOptions);
Console.WriteLine();
Console.WriteLine($"{prefix}\n{pretty}");
}
catch (JsonException)
{
Console.WriteLine();
Console.WriteLine(line);
}
}
public async ValueTask DisposeAsync()
{
try
{
await _writer.FlushAsync().ConfigureAwait(false);
}
catch (IOException)
{
}
catch (ObjectDisposedException)
{
}
_writeLock.Dispose();
_reader.Dispose();
await _writer.DisposeAsync().ConfigureAwait(false);
_client.Dispose();
}
}
-193
View File
@@ -1,193 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Net.Sockets;
using SharpEmu.DebugClient;
return await ClientProgram.RunAsync(args).ConfigureAwait(false);
internal static class ClientProgram
{
public static async Task<int> RunAsync(string[] args)
{
if (args.Any(a => a is "--help" or "-h"))
{
PrintUsage();
return 0;
}
string? endpointArg = null;
var execCommands = new List<string>();
var quiet = false;
for (var i = 0; i < args.Length; i++)
{
var arg = args[i];
if (string.Equals(arg, "--exec", StringComparison.OrdinalIgnoreCase) || string.Equals(arg, "-e", StringComparison.OrdinalIgnoreCase))
{
if (i + 1 >= args.Length)
{
Console.Error.WriteLine("--exec requires a command argument.");
return 2;
}
execCommands.Add(args[++i]);
continue;
}
if (string.Equals(arg, "--quiet", StringComparison.OrdinalIgnoreCase))
{
quiet = true;
continue;
}
if (arg.StartsWith('-'))
{
Console.Error.WriteLine($"Unknown option '{arg}'.");
PrintUsage();
return 2;
}
endpointArg ??= arg;
}
if (!ClientEndpoint.TryParse(endpointArg, out var host, out var port, out var endpointError))
{
Console.Error.WriteLine(endpointError);
return 2;
}
using var shutdown = new CancellationTokenSource();
Console.CancelKeyPress += (_, eventArgs) =>
{
eventArgs.Cancel = true;
shutdown.Cancel();
};
DebugClientConnection connection;
try
{
connection = await DebugClientConnection.ConnectAsync(host, port, shutdown.Token).ConfigureAwait(false);
}
catch (Exception ex) when (ex is SocketException or OperationCanceledException)
{
Console.Error.WriteLine($"Could not connect to {host}:{port}: {ex.Message}");
Console.Error.WriteLine("Start the emulator with --debug-server first.");
return 3;
}
await using (connection)
{
var receiveTask = connection.ReceiveLoopAsync(shutdown.Token);
if (execCommands.Count > 0)
{
await RunOneShotAsync(connection, execCommands, shutdown.Token).ConfigureAwait(false);
}
else
{
if (!quiet)
{
Console.WriteLine($"Connected to SharpEmu debug server at {host}:{port}.");
Console.WriteLine("Type 'help' for commands, 'quit' to exit.");
}
await RunReplAsync(connection, shutdown).ConfigureAwait(false);
}
shutdown.Cancel();
try
{
await receiveTask.ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
}
return 0;
}
private static async Task RunOneShotAsync(
DebugClientConnection connection,
IReadOnlyList<string> commands,
CancellationToken cancellationToken)
{
foreach (var command in commands)
{
var result = CommandTranslator.Translate(command);
switch (result.Kind)
{
case CommandTranslator.ActionKind.SendRequest:
await connection.SendAsync(result.Payload!, cancellationToken).ConfigureAwait(false);
break;
case CommandTranslator.ActionKind.Error:
Console.Error.WriteLine(result.Error);
break;
case CommandTranslator.ActionKind.ShowHelp:
Console.WriteLine(CommandTranslator.HelpText);
break;
}
}
// Give the server a moment to answer before the client exits.
try
{
await Task.Delay(TimeSpan.FromMilliseconds(500), cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
}
}
private static async Task RunReplAsync(DebugClientConnection connection, CancellationTokenSource shutdown)
{
while (!shutdown.IsCancellationRequested)
{
var line = await Console.In.ReadLineAsync(shutdown.Token).ConfigureAwait(false);
if (line is null)
{
break;
}
var result = CommandTranslator.Translate(line);
switch (result.Kind)
{
case CommandTranslator.ActionKind.Quit:
return;
case CommandTranslator.ActionKind.ShowHelp:
Console.WriteLine(CommandTranslator.HelpText);
break;
case CommandTranslator.ActionKind.Error:
Console.Error.WriteLine(result.Error);
break;
case CommandTranslator.ActionKind.Ignore:
break;
case CommandTranslator.ActionKind.SendRequest:
try
{
await connection.SendAsync(result.Payload!, shutdown.Token).ConfigureAwait(false);
}
catch (Exception ex) when (ex is IOException or ObjectDisposedException)
{
Console.Error.WriteLine("Send failed; the connection is closed.");
return;
}
break;
}
}
}
private static void PrintUsage()
{
Console.WriteLine("SharpEmu.DebugClient — live debugger client for the SharpEmu debug server.");
Console.WriteLine();
Console.WriteLine("Usage: SharpEmu.DebugClient [host:port] [--exec \"<command>\"]... [--quiet]");
Console.WriteLine(" host:port Server endpoint (default 127.0.0.1:5714).");
Console.WriteLine(" --exec, -e Run a command non-interactively (repeatable), then exit.");
Console.WriteLine(" --quiet Suppress the connection banner.");
Console.WriteLine(" --help, -h Show this help.");
Console.WriteLine();
Console.WriteLine(CommandTranslator.HelpText);
}
}
@@ -1,16 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<!-- A small, standalone console tool: it speaks the debug server's
line-delimited JSON protocol directly over TCP and takes no dependency
on the emulator assemblies, so it builds and ships independently. -->
<OutputType>Exe</OutputType>
<AssemblyName>SharpEmu.DebugClient</AssemblyName>
<RootNamespace>SharpEmu.DebugClient</RootNamespace>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
</Project>
@@ -1,48 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Debugger.Breakpoints;
/// <summary>
/// A single breakpoint or watchpoint. Instances are immutable; the owning
/// <see cref="BreakpointStore"/> replaces an entry to change its enabled state.
/// </summary>
public sealed class Breakpoint
{
public Breakpoint(int id, BreakpointKind kind, ulong address, ulong length = 1, bool enabled = true)
{
if (length == 0)
{
throw new ArgumentOutOfRangeException(nameof(length), "Breakpoint length must be at least one byte.");
}
Id = id;
Kind = kind;
Address = address;
Length = length;
Enabled = enabled;
}
/// <summary>The store-assigned identifier used by clients to reference it.</summary>
public int Id { get; }
public BreakpointKind Kind { get; }
/// <summary>The first guest address the breakpoint covers.</summary>
public ulong Address { get; }
/// <summary>
/// The number of bytes the breakpoint covers. Always one for
/// <see cref="BreakpointKind.Execute"/>; the watch kinds may span a range.
/// </summary>
public ulong Length { get; }
public bool Enabled { get; }
/// <summary>True when <paramref name="address"/> falls within this breakpoint.</summary>
public bool Covers(ulong address) => address >= Address && address < Address + Length;
/// <summary>Returns a copy with a different enabled state.</summary>
public Breakpoint WithEnabled(bool enabled)
=> enabled == Enabled ? this : new Breakpoint(Id, Kind, Address, Length, enabled);
}
@@ -1,26 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Debugger.Breakpoints;
/// <summary>
/// The kind of stop a breakpoint requests. Execution breakpoints are honoured
/// at the frame-boundary seam that exists today; the data-watch kinds are part
/// of the surface so client protocols and tooling can be built against them,
/// and are armed once the execution backend can report the corresponding
/// accesses.
/// </summary>
public enum BreakpointKind
{
/// <summary>Stop when the instruction pointer reaches the address.</summary>
Execute,
/// <summary>Stop when the guest reads from the address range.</summary>
ReadWatch,
/// <summary>Stop when the guest writes to the address range.</summary>
WriteWatch,
/// <summary>Stop when the guest reads from or writes to the address range.</summary>
AccessWatch,
}
@@ -1,90 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Debugger.Breakpoints;
/// <summary>
/// A thread-safe registry of breakpoints. The debug server mutates it from
/// client-servicing threads while the emulation thread queries it at frame
/// boundaries, so every operation takes the same lock.
/// </summary>
public sealed class BreakpointStore
{
private readonly object _sync = new();
private readonly Dictionary<int, Breakpoint> _breakpoints = new();
private int _nextId = 1;
/// <summary>Adds a breakpoint and returns the created entry with its id.</summary>
public Breakpoint Add(BreakpointKind kind, ulong address, ulong length = 1)
{
lock (_sync)
{
var effectiveLength = kind == BreakpointKind.Execute ? 1UL : Math.Max(1UL, length);
var breakpoint = new Breakpoint(_nextId++, kind, address, effectiveLength);
_breakpoints[breakpoint.Id] = breakpoint;
return breakpoint;
}
}
/// <summary>Removes a breakpoint by id. Returns false when it did not exist.</summary>
public bool Remove(int id)
{
lock (_sync)
{
return _breakpoints.Remove(id);
}
}
/// <summary>Enables or disables a breakpoint by id.</summary>
public bool SetEnabled(int id, bool enabled)
{
lock (_sync)
{
if (!_breakpoints.TryGetValue(id, out var breakpoint))
{
return false;
}
_breakpoints[id] = breakpoint.WithEnabled(enabled);
return true;
}
}
/// <summary>Removes every breakpoint.</summary>
public void Clear()
{
lock (_sync)
{
_breakpoints.Clear();
}
}
/// <summary>Returns a point-in-time copy of all breakpoints.</summary>
public IReadOnlyList<Breakpoint> Snapshot()
{
lock (_sync)
{
return _breakpoints.Values.ToArray();
}
}
/// <summary>
/// Finds the first enabled execution breakpoint covering <paramref name="address"/>,
/// or null when none applies.
/// </summary>
public Breakpoint? FindExecuteHit(ulong address)
{
lock (_sync)
{
foreach (var breakpoint in _breakpoints.Values)
{
if (breakpoint.Enabled && breakpoint.Kind == BreakpointKind.Execute && breakpoint.Covers(address))
{
return breakpoint;
}
}
return null;
}
}
}
@@ -1,73 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.HLE;
namespace SharpEmu.Debugger;
/// <summary>
/// An immutable snapshot of the guest integer register state at a stop. XMM/YMM
/// state is intentionally omitted here and read on demand through the target to
/// keep the common register-dump path cheap.
/// </summary>
public readonly struct DebugRegisterFile
{
private readonly ulong[] _generalPurpose;
public DebugRegisterFile(
ulong[] generalPurpose,
ulong rip,
ulong rflags,
ulong fsBase,
ulong gsBase)
{
ArgumentNullException.ThrowIfNull(generalPurpose);
if (generalPurpose.Length != 16)
{
throw new ArgumentException("Expected 16 general-purpose registers.", nameof(generalPurpose));
}
_generalPurpose = generalPurpose;
Rip = rip;
Rflags = rflags;
FsBase = fsBase;
GsBase = gsBase;
}
public ulong Rip { get; }
public ulong Rflags { get; }
public ulong FsBase { get; }
public ulong GsBase { get; }
/// <summary>Reads a register by identifier.</summary>
public ulong this[DebugRegisterId id] => id switch
{
DebugRegisterId.Rip => Rip,
DebugRegisterId.Rflags => Rflags,
DebugRegisterId.FsBase => FsBase,
DebugRegisterId.GsBase => GsBase,
_ when id.IsGeneralPurpose() => _generalPurpose[(int)id],
_ => throw new ArgumentOutOfRangeException(nameof(id), id, null),
};
/// <summary>Reads a general-purpose register.</summary>
public ulong this[CpuRegister register] => _generalPurpose[(int)register];
/// <summary>Captures the integer register state of a live debug frame.</summary>
public static DebugRegisterFile Capture(ICpuDebugFrame frame)
{
ArgumentNullException.ThrowIfNull(frame);
var gpr = new ulong[16];
for (var i = 0; i < gpr.Length; i++)
{
gpr[i] = frame.GetRegister((CpuRegister)i);
}
return new DebugRegisterFile(gpr, frame.Rip, frame.Rflags, frame.FsBase, frame.GsBase);
}
}
-62
View File
@@ -1,62 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Debugger;
/// <summary>
/// The registers a debugger can name. The first sixteen values line up with
/// <see cref="CpuRegister"/> so a general-purpose register can be converted
/// between the two enums by casting; the remaining values cover the special
/// registers a debug frame exposes.
/// </summary>
public enum DebugRegisterId
{
Rax = 0,
Rcx = 1,
Rdx = 2,
Rbx = 3,
Rsp = 4,
Rbp = 5,
Rsi = 6,
Rdi = 7,
R8 = 8,
R9 = 9,
R10 = 10,
R11 = 11,
R12 = 12,
R13 = 13,
R14 = 14,
R15 = 15,
Rip = 16,
Rflags = 17,
FsBase = 18,
GsBase = 19,
}
/// <summary>Helpers for mapping between debug and CPU register identifiers.</summary>
public static class DebugRegisterIdExtensions
{
/// <summary>
/// True when the identifier names one of the sixteen general-purpose
/// registers and can be cast to <see cref="CpuRegister"/>.
/// </summary>
public static bool IsGeneralPurpose(this DebugRegisterId id)
=> id is >= DebugRegisterId.Rax and <= DebugRegisterId.R15;
/// <summary>
/// Converts a general-purpose identifier to its <see cref="CpuRegister"/>.
/// Throws when <paramref name="id"/> is a special register.
/// </summary>
public static CpuRegister ToCpuRegister(this DebugRegisterId id)
{
if (!id.IsGeneralPurpose())
{
throw new ArgumentOutOfRangeException(nameof(id), id, "Not a general-purpose register.");
}
return (CpuRegister)(int)id;
}
}
@@ -1,59 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Net;
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.Debugger.Server;
using SharpEmu.Debugger.Session;
namespace SharpEmu.Debugger;
/// <summary>
/// One-call wiring of the live debugger: it owns a <see cref="DebuggerSession"/>
/// and a <see cref="DebuggerServer"/>, exposes the <see cref="Hook"/> to attach
/// to <c>SharpEmuRuntimeOptions.DebugHook</c>, and starts/stops the network
/// front-end. A host constructs one, hands <see cref="Hook"/> to the runtime,
/// calls <see cref="Start"/>, and calls <see cref="NotifyRunCompleted"/> once the
/// runtime returns.
/// </summary>
public sealed class DebuggerServerHost : IAsyncDisposable
{
private readonly DebuggerSession _session;
private readonly DebuggerServer _server;
public DebuggerServerHost(
DebuggerServerOptions? serverOptions = null,
DebuggerSessionOptions? sessionOptions = null)
{
_session = new DebuggerSession(sessionOptions);
_server = new DebuggerServer(_session, serverOptions);
}
/// <summary>The session driving the target.</summary>
public IDebuggerSession Session => _session;
/// <summary>
/// The dispatcher hook to hand to the runtime so guest frames route through
/// the debugger.
/// </summary>
public ICpuDebugHook Hook => _session.Hook;
/// <summary>The endpoint the server bound to, or null before <see cref="Start"/>.</summary>
public IPEndPoint? Endpoint => _server.Endpoint;
/// <summary>Begins accepting debugger clients.</summary>
public void Start() => _server.Start();
/// <summary>
/// Releases a parked emulation thread and marks the target terminated. Call
/// after the runtime's run returns so any attached client is notified and the
/// guest thread is never left blocked in the debugger.
/// </summary>
public void NotifyRunCompleted() => _session.NotifyTerminated();
public async ValueTask DisposeAsync()
{
_session.NotifyTerminated();
await _server.DisposeAsync().ConfigureAwait(false);
}
}
@@ -1,340 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Debugger.Breakpoints;
using SharpEmu.Debugger.Session;
using SharpEmu.HLE;
namespace SharpEmu.Debugger.Protocol;
/// <summary>
/// Translates parsed <see cref="DebugRequest"/> verbs into operations on an
/// <see cref="IDebuggerSession"/> and packages the outcome as a
/// <see cref="DebugResponse"/>. This is the single place command semantics live,
/// so it is shared by every connection and independent of the wire format.
/// </summary>
public sealed class DebugCommandDispatcher
{
private readonly IDebuggerSession _session;
public DebugCommandDispatcher(IDebuggerSession session)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
}
public DebugResponse Dispatch(DebugRequest request)
{
return request.Command switch
{
JsonLineDebugProtocol.ParseErrorCommand => ParseError(request),
"ping" => DebugResponse.Success(request.Command),
"status" or "info" => Status(request),
"state" => DebugResponse.Success(request.Command, new Dictionary<string, object?>
{
["state"] = _session.State.ToString(),
}),
"registers" or "regs" => Registers(request),
"set-register" or "set-reg" => SetRegister(request),
"read-memory" or "read-mem" => ReadMemory(request),
"write-memory" or "write-mem" => WriteMemory(request),
"list-breakpoints" or "breakpoints" => ListBreakpoints(request),
"add-breakpoint" or "break" => AddBreakpoint(request),
"remove-breakpoint" or "delete-breakpoint" => RemoveBreakpoint(request),
"enable-breakpoint" => EnableBreakpoint(request),
"continue" or "cont" or "c" => Simple(request, _session.Continue(), "Target is not paused."),
"step" or "s" => Simple(request, _session.StepFrame(), "Target is not paused."),
"pause" => Pause(request),
_ => DebugResponse.Failure(request.Command, $"Unknown command '{request.Command}'."),
};
}
private static DebugResponse ParseError(DebugRequest request)
{
var message = request.TryGetString("message", out var text) ? text : "Malformed request.";
return DebugResponse.Failure(request.Command, message);
}
private DebugResponse Status(DebugRequest request)
{
var data = new Dictionary<string, object?>
{
["state"] = _session.State.ToString(),
["breakpoints"] = _session.Breakpoints.Snapshot().Count,
};
if (_session.LastStop is { } stop)
{
data["lastStop"] = DescribeStop(stop);
}
return DebugResponse.Success(request.Command, data);
}
private DebugResponse Registers(DebugRequest request)
{
if (!_session.TryGetRegisters(out var registers))
{
return NotPaused(request);
}
return DebugResponse.Success(request.Command, new Dictionary<string, object?>
{
["registers"] = DescribeRegisters(registers),
});
}
private DebugResponse SetRegister(DebugRequest request)
{
if (!request.TryGetString("register", out var name) || !TryParseRegister(name, out var id))
{
return DebugResponse.Failure(request.Command, "Expected a valid 'register' name.");
}
if (!request.TryGetUInt64("value", out var value))
{
return DebugResponse.Failure(request.Command, "Expected a 'value'.");
}
return _session.TrySetRegister(id, value)
? DebugResponse.Success(request.Command)
: DebugResponse.Failure(request.Command, "Register is not writable or target is not paused.");
}
private DebugResponse ReadMemory(DebugRequest request)
{
if (!request.TryGetUInt64("address", out var address))
{
return DebugResponse.Failure(request.Command, "Expected an 'address'.");
}
if (!request.TryGetInt32("length", out var length) || length <= 0 || length > MaxMemoryChunk)
{
return DebugResponse.Failure(request.Command, $"Expected a 'length' between 1 and {MaxMemoryChunk}.");
}
var buffer = new byte[length];
if (!_session.TryReadMemory(address, buffer))
{
return DebugResponse.Failure(request.Command, "Memory is unreadable or target is not paused.");
}
return DebugResponse.Success(request.Command, new Dictionary<string, object?>
{
["address"] = FormatAddress(address),
["length"] = length,
["bytes"] = Convert.ToHexString(buffer),
});
}
private DebugResponse WriteMemory(DebugRequest request)
{
if (!request.TryGetUInt64("address", out var address))
{
return DebugResponse.Failure(request.Command, "Expected an 'address'.");
}
if (!request.TryGetString("bytes", out var hex) || hex.Length == 0 || (hex.Length & 1) != 0)
{
return DebugResponse.Failure(request.Command, "Expected 'bytes' as an even-length hex string.");
}
byte[] data;
try
{
data = Convert.FromHexString(hex);
}
catch (FormatException)
{
return DebugResponse.Failure(request.Command, "'bytes' is not valid hex.");
}
if (data.Length > MaxMemoryChunk)
{
return DebugResponse.Failure(request.Command, $"Cannot write more than {MaxMemoryChunk} bytes at once.");
}
return _session.TryWriteMemory(address, data)
? DebugResponse.Success(request.Command, new Dictionary<string, object?> { ["written"] = data.Length })
: DebugResponse.Failure(request.Command, "Memory is unwritable or target is not paused.");
}
private DebugResponse ListBreakpoints(DebugRequest request)
{
var breakpoints = _session.Breakpoints.Snapshot()
.OrderBy(breakpoint => breakpoint.Id)
.Select(DescribeBreakpoint)
.ToArray();
return DebugResponse.Success(request.Command, new Dictionary<string, object?>
{
["breakpoints"] = breakpoints,
});
}
private DebugResponse AddBreakpoint(DebugRequest request)
{
if (!request.TryGetUInt64("address", out var address))
{
return DebugResponse.Failure(request.Command, "Expected an 'address'.");
}
var kind = BreakpointKind.Execute;
if (request.TryGetString("kind", out var kindText) && !TryParseBreakpointKind(kindText, out kind))
{
return DebugResponse.Failure(request.Command, $"Unknown breakpoint kind '{kindText}'.");
}
var length = 1UL;
if (request.TryGetUInt64("length", out var requestedLength) && requestedLength > 0)
{
length = requestedLength;
}
var breakpoint = _session.Breakpoints.Add(kind, address, length);
return DebugResponse.Success(request.Command, new Dictionary<string, object?>
{
["breakpoint"] = DescribeBreakpoint(breakpoint),
});
}
private DebugResponse RemoveBreakpoint(DebugRequest request)
{
if (!request.TryGetInt32("id", out var id))
{
return DebugResponse.Failure(request.Command, "Expected an 'id'.");
}
return _session.Breakpoints.Remove(id)
? DebugResponse.Success(request.Command)
: DebugResponse.Failure(request.Command, $"No breakpoint with id {id}.");
}
private DebugResponse EnableBreakpoint(DebugRequest request)
{
if (!request.TryGetInt32("id", out var id))
{
return DebugResponse.Failure(request.Command, "Expected an 'id'.");
}
var enabled = !request.TryGetBool("enabled", out var requested) || requested;
return _session.Breakpoints.SetEnabled(id, enabled)
? DebugResponse.Success(request.Command)
: DebugResponse.Failure(request.Command, $"No breakpoint with id {id}.");
}
private DebugResponse Pause(DebugRequest request)
{
_session.RequestPause();
return DebugResponse.Success(request.Command);
}
private static DebugResponse Simple(DebugRequest request, bool succeeded, string failureMessage)
=> succeeded ? DebugResponse.Success(request.Command) : DebugResponse.Failure(request.Command, failureMessage);
private static DebugResponse NotPaused(DebugRequest request)
=> DebugResponse.Failure(request.Command, "Target is not paused.");
internal static IReadOnlyDictionary<string, object?> DescribeStop(DebugStopEvent stop)
{
var data = new Dictionary<string, object?>
{
["reason"] = stop.Reason.ToString(),
["address"] = FormatAddress(stop.Address),
["frameKind"] = stop.FrameKind.ToString(),
["frameLabel"] = stop.FrameLabel,
["registers"] = DescribeRegisters(stop.Registers),
};
if (stop.Breakpoint is { } breakpoint)
{
data["breakpoint"] = DescribeBreakpoint(breakpoint);
}
if (stop.Result is { } result)
{
data["result"] = result.ToString();
}
if (stop.Detail is { } detail)
{
data["detail"] = detail;
}
if (stop.OpcodeBytes is { } opcodeBytes)
{
data["opcodeBytes"] = opcodeBytes;
}
if (stop.StallInfo is { } stall)
{
data["stall"] = new Dictionary<string, object?>
{
["kind"] = stall.Kind.ToString(),
["nid"] = stall.Nid,
["instructionPointer"] = FormatAddress(stall.InstructionPointer),
["dispatchIndex"] = stall.DispatchIndex,
["argument0"] = FormatAddress(stall.Argument0),
["argument1"] = FormatAddress(stall.Argument1),
["resolved"] = stall.IsResolved,
["library"] = stall.LibraryName,
["function"] = stall.FunctionName,
};
}
return data;
}
private static IReadOnlyDictionary<string, object?> DescribeRegisters(DebugRegisterFile registers)
{
var result = new Dictionary<string, object?>(20);
for (var i = 0; i < 16; i++)
{
result[((CpuRegister)i).ToString().ToLowerInvariant()] = FormatAddress(registers[(CpuRegister)i]);
}
result["rip"] = FormatAddress(registers.Rip);
result["rflags"] = FormatAddress(registers.Rflags);
result["fs_base"] = FormatAddress(registers.FsBase);
result["gs_base"] = FormatAddress(registers.GsBase);
return result;
}
private static IReadOnlyDictionary<string, object?> DescribeBreakpoint(Breakpoint breakpoint)
=> new Dictionary<string, object?>
{
["id"] = breakpoint.Id,
["kind"] = breakpoint.Kind.ToString(),
["address"] = FormatAddress(breakpoint.Address),
["length"] = breakpoint.Length,
["enabled"] = breakpoint.Enabled,
};
private static string FormatAddress(ulong value) => $"0x{value:X16}";
private static bool TryParseRegister(string name, out DebugRegisterId id)
{
var normalized = name.Trim().ToLowerInvariant();
switch (normalized)
{
case "rip":
id = DebugRegisterId.Rip;
return true;
case "rflags":
id = DebugRegisterId.Rflags;
return true;
case "fs_base" or "fsbase":
id = DebugRegisterId.FsBase;
return true;
case "gs_base" or "gsbase":
id = DebugRegisterId.GsBase;
return true;
}
return Enum.TryParse(normalized, ignoreCase: true, out id) && Enum.IsDefined(id);
}
private static bool TryParseBreakpointKind(string text, out BreakpointKind kind)
=> Enum.TryParse(text.Trim(), ignoreCase: true, out kind) && Enum.IsDefined(kind);
private const int MaxMemoryChunk = 64 * 1024;
}
@@ -1,152 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Globalization;
using System.Text.Json;
namespace SharpEmu.Debugger.Protocol;
/// <summary>
/// A parsed client request: a <see cref="Command"/> verb plus a bag of named
/// arguments backed by the original JSON. Numeric arguments accept either JSON
/// numbers or <c>"0x"</c>-prefixed hex strings so addresses read naturally on
/// the wire.
/// </summary>
public sealed class DebugRequest
{
private readonly JsonElement _root;
private DebugRequest(string command, JsonElement root)
{
Command = command;
_root = root;
}
/// <summary>The lower-cased command verb.</summary>
public string Command { get; }
/// <summary>
/// Parses a single JSON object into a request. Returns false when the text is
/// not a JSON object or is missing a string <c>command</c> field.
/// </summary>
public static bool TryParse(string json, out DebugRequest request, out string error)
{
request = null!;
error = string.Empty;
try
{
using var document = JsonDocument.Parse(json);
var root = document.RootElement.Clone();
if (root.ValueKind != JsonValueKind.Object)
{
error = "Request must be a JSON object.";
return false;
}
if (!root.TryGetProperty("command", out var commandElement) ||
commandElement.ValueKind != JsonValueKind.String)
{
error = "Request is missing a string 'command'.";
return false;
}
var command = commandElement.GetString() ?? string.Empty;
request = new DebugRequest(command.Trim().ToLowerInvariant(), root);
return true;
}
catch (JsonException ex)
{
error = $"Malformed JSON: {ex.Message}";
return false;
}
}
public bool TryGetString(string name, out string value)
{
if (_root.TryGetProperty(name, out var element) && element.ValueKind == JsonValueKind.String)
{
value = element.GetString() ?? string.Empty;
return true;
}
value = string.Empty;
return false;
}
public bool TryGetUInt64(string name, out ulong value)
{
value = 0;
if (!_root.TryGetProperty(name, out var element))
{
return false;
}
switch (element.ValueKind)
{
case JsonValueKind.Number:
return element.TryGetUInt64(out value);
case JsonValueKind.String:
return TryParseNumber(element.GetString(), out value);
default:
return false;
}
}
public bool TryGetInt32(string name, out int value)
{
value = 0;
if (!_root.TryGetProperty(name, out var element))
{
return false;
}
switch (element.ValueKind)
{
case JsonValueKind.Number:
return element.TryGetInt32(out value);
case JsonValueKind.String when TryParseNumber(element.GetString(), out var parsed) && parsed <= int.MaxValue:
value = (int)parsed;
return true;
default:
return false;
}
}
public bool TryGetBool(string name, out bool value)
{
value = false;
if (!_root.TryGetProperty(name, out var element))
{
return false;
}
switch (element.ValueKind)
{
case JsonValueKind.True:
value = true;
return true;
case JsonValueKind.False:
value = false;
return true;
default:
return false;
}
}
private static bool TryParseNumber(string? text, out ulong value)
{
value = 0;
if (string.IsNullOrWhiteSpace(text))
{
return false;
}
text = text.Trim();
if (text.StartsWith("0x", StringComparison.OrdinalIgnoreCase))
{
return ulong.TryParse(text.AsSpan(2), NumberStyles.HexNumber, CultureInfo.InvariantCulture, out value);
}
return ulong.TryParse(text, NumberStyles.Integer, CultureInfo.InvariantCulture, out value);
}
}
@@ -1,34 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Debugger.Protocol;
/// <summary>
/// The reply to a <see cref="DebugRequest"/>: either success with an optional
/// data payload, or a failure with a human-readable message.
/// </summary>
public sealed class DebugResponse
{
private DebugResponse(bool ok, string? command, IReadOnlyDictionary<string, object?>? data, string? error)
{
Ok = ok;
Command = command;
Data = data;
Error = error;
}
public bool Ok { get; }
/// <summary>Echoes the command the reply answers, when known.</summary>
public string? Command { get; }
public IReadOnlyDictionary<string, object?>? Data { get; }
public string? Error { get; }
public static DebugResponse Success(string command, IReadOnlyDictionary<string, object?>? data = null)
=> new(ok: true, command, data, error: null);
public static DebugResponse Failure(string command, string error)
=> new(ok: false, command, data: null, error);
}
@@ -1,33 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Debugger.Protocol;
/// <summary>
/// Frames debugger traffic on a connection. A protocol turns bytes into
/// <see cref="DebugRequest"/> objects and serialises <see cref="DebugResponse"/>
/// replies plus asynchronous events (stops, resumes, termination) back to the
/// client. Swapping the implementation (line-delimited JSON today, a GDB remote
/// serial stub later) leaves the session and server untouched.
/// </summary>
public interface IDebugProtocol
{
/// <summary>A short protocol name reported in the handshake.</summary>
string Name { get; }
/// <summary>
/// Reads the next request, or null at end of stream. Parse failures are
/// surfaced as a request with a reserved error command rather than throwing.
/// </summary>
Task<DebugRequest?> ReadRequestAsync(TextReader reader, CancellationToken cancellationToken);
/// <summary>Writes a reply to a request.</summary>
Task WriteResponseAsync(TextWriter writer, DebugResponse response, CancellationToken cancellationToken);
/// <summary>Writes an unsolicited event (for example a stop notification).</summary>
Task WriteEventAsync(
TextWriter writer,
string eventName,
IReadOnlyDictionary<string, object?> data,
CancellationToken cancellationToken);
}
@@ -1,112 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text.Json;
namespace SharpEmu.Debugger.Protocol;
/// <summary>
/// A newline-delimited JSON protocol: one JSON object per line in each
/// direction. Requests carry a <c>command</c>; replies carry <c>ok</c> plus
/// <c>data</c>/<c>error</c>; events carry an <c>event</c> name. It is trivial to
/// drive from a socket, <c>nc</c>, or a small script, which suits bring-up and
/// tooling while a richer protocol is layered on later.
/// </summary>
public sealed class JsonLineDebugProtocol : IDebugProtocol
{
/// <summary>The command assigned to a request that failed to parse.</summary>
public const string ParseErrorCommand = "$parse-error";
private static readonly JsonSerializerOptions SerializerOptions = new()
{
WriteIndented = false,
};
public string Name => "json-lines/1";
public async Task<DebugRequest?> ReadRequestAsync(TextReader reader, CancellationToken cancellationToken)
{
while (true)
{
cancellationToken.ThrowIfCancellationRequested();
var line = await reader.ReadLineAsync(cancellationToken).ConfigureAwait(false);
if (line is null)
{
return null;
}
if (string.IsNullOrWhiteSpace(line))
{
continue;
}
if (DebugRequest.TryParse(line, out var request, out var error))
{
return request;
}
// Surface the parse failure as a synthetic request so the connection
// loop can reply with an error rather than dropping the client.
var envelope = $"{{\"command\":\"{ParseErrorCommand}\",\"message\":{JsonSerializer.Serialize(error)}}}";
if (DebugRequest.TryParse(envelope, out var errorRequest, out _))
{
return errorRequest;
}
}
}
public async Task WriteResponseAsync(TextWriter writer, DebugResponse response, CancellationToken cancellationToken)
{
var payload = new Dictionary<string, object?>
{
["ok"] = response.Ok,
};
if (response.Command is not null)
{
payload["command"] = response.Command;
}
if (response.Data is not null)
{
payload["data"] = response.Data;
}
if (response.Error is not null)
{
payload["error"] = response.Error;
}
await WriteLineAsync(writer, payload, cancellationToken).ConfigureAwait(false);
}
public async Task WriteEventAsync(
TextWriter writer,
string eventName,
IReadOnlyDictionary<string, object?> data,
CancellationToken cancellationToken)
{
var payload = new Dictionary<string, object?>(data.Count + 1)
{
["event"] = eventName,
};
foreach (var (key, value) in data)
{
payload[key] = value;
}
await WriteLineAsync(writer, payload, cancellationToken).ConfigureAwait(false);
}
private static async Task WriteLineAsync(
TextWriter writer,
IReadOnlyDictionary<string, object?> payload,
CancellationToken cancellationToken)
{
cancellationToken.ThrowIfCancellationRequested();
var json = JsonSerializer.Serialize(payload, SerializerOptions);
await writer.WriteLineAsync(json.AsMemory(), cancellationToken).ConfigureAwait(false);
await writer.FlushAsync(cancellationToken).ConfigureAwait(false);
}
}
@@ -1,158 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Net.Sockets;
using System.Text;
using SharpEmu.Debugger.Protocol;
using SharpEmu.Debugger.Session;
using SharpEmu.Logging;
namespace SharpEmu.Debugger.Server;
/// <summary>
/// Services a single connected client: reads requests, dispatches them against
/// the shared session, and pushes session lifecycle events. Writes from the
/// request loop and from event callbacks are serialised through one lock so the
/// two never interleave a half-written line.
/// </summary>
internal sealed class DebuggerClientConnection : IAsyncDisposable
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.Debugger");
private static readonly UTF8Encoding Utf8NoBom = new(encoderShouldEmitUTF8Identifier: false);
private readonly TcpClient _client;
private readonly IDebuggerSession _session;
private readonly IDebugProtocol _protocol;
private readonly DebugCommandDispatcher _dispatcher;
private readonly SemaphoreSlim _writeLock = new(1, 1);
private TextWriter? _writer;
private CancellationToken _cancellationToken;
public DebuggerClientConnection(TcpClient client, IDebuggerSession session, IDebugProtocol protocol)
{
_client = client;
_session = session;
_protocol = protocol;
_dispatcher = new DebugCommandDispatcher(session);
}
public async Task RunAsync(CancellationToken cancellationToken)
{
_cancellationToken = cancellationToken;
var endpoint = _client.Client.RemoteEndPoint?.ToString() ?? "unknown";
Log.Info($"Debugger client connected: {endpoint}");
using var stream = _client.GetStream();
using var reader = new StreamReader(stream, Utf8NoBom);
await using var writer = new StreamWriter(stream, Utf8NoBom) { AutoFlush = false };
_writer = writer;
_session.Stopped += OnStopped;
_session.Resumed += OnResumed;
_session.Terminated += OnTerminated;
try
{
await SendEventAsync("hello", new Dictionary<string, object?>
{
["protocol"] = _protocol.Name,
["state"] = _session.State.ToString(),
}).ConfigureAwait(false);
while (!cancellationToken.IsCancellationRequested)
{
var request = await _protocol.ReadRequestAsync(reader, cancellationToken).ConfigureAwait(false);
if (request is null)
{
break;
}
var response = _dispatcher.Dispatch(request);
await WriteResponseAsync(response).ConfigureAwait(false);
}
}
catch (OperationCanceledException)
{
// Server shutting down.
}
catch (IOException)
{
// Client dropped the connection.
}
catch (Exception ex)
{
Log.Warn($"Debugger client error ({endpoint}): {ex.Message}");
}
finally
{
_session.Stopped -= OnStopped;
_session.Resumed -= OnResumed;
_session.Terminated -= OnTerminated;
_writer = null;
Log.Info($"Debugger client disconnected: {endpoint}");
}
}
private void OnStopped(object? sender, DebugStopEvent stop)
=> _ = SendEventAsync("stopped", DebugCommandDispatcher.DescribeStop(stop));
private void OnResumed(object? sender, EventArgs e)
=> _ = SendEventAsync("resumed", EmptyData);
private void OnTerminated(object? sender, EventArgs e)
=> _ = SendEventAsync("terminated", EmptyData);
private async Task WriteResponseAsync(DebugResponse response)
{
var writer = _writer;
if (writer is null)
{
return;
}
await _writeLock.WaitAsync(_cancellationToken).ConfigureAwait(false);
try
{
await _protocol.WriteResponseAsync(writer, response, _cancellationToken).ConfigureAwait(false);
}
finally
{
_writeLock.Release();
}
}
private async Task SendEventAsync(string name, IReadOnlyDictionary<string, object?> data)
{
var writer = _writer;
if (writer is null)
{
return;
}
try
{
await _writeLock.WaitAsync(_cancellationToken).ConfigureAwait(false);
try
{
await _protocol.WriteEventAsync(writer, name, data, _cancellationToken).ConfigureAwait(false);
}
finally
{
_writeLock.Release();
}
}
catch (Exception ex) when (ex is IOException or OperationCanceledException or ObjectDisposedException)
{
// The client went away between the event firing and the write.
}
}
public ValueTask DisposeAsync()
{
_writeLock.Dispose();
_client.Dispose();
return ValueTask.CompletedTask;
}
private static readonly IReadOnlyDictionary<string, object?> EmptyData = new Dictionary<string, object?>();
}
@@ -1,136 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Net;
using System.Net.Sockets;
using SharpEmu.Debugger.Protocol;
using SharpEmu.Debugger.Session;
using SharpEmu.Logging;
namespace SharpEmu.Debugger.Server;
/// <summary>
/// A TCP server that exposes an <see cref="IDebuggerSession"/> to remote
/// clients over a pluggable <see cref="IDebugProtocol"/>. Every connection sees
/// the same session, so multiple clients (for example a UI and a scripted
/// probe) observe a consistent view of the target.
/// </summary>
public sealed class DebuggerServer : IDebuggerServer
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.Debugger");
private readonly IDebuggerSession _session;
private readonly DebuggerServerOptions _options;
private readonly Func<IDebugProtocol> _protocolFactory;
private readonly ConcurrentDictionary<DebuggerClientConnection, Task> _connections = new();
private readonly CancellationTokenSource _shutdown = new();
private TcpListener? _listener;
private Task? _acceptLoop;
public DebuggerServer(
IDebuggerSession session,
DebuggerServerOptions? options = null,
Func<IDebugProtocol>? protocolFactory = null)
{
_session = session ?? throw new ArgumentNullException(nameof(session));
_options = options ?? new DebuggerServerOptions();
_protocolFactory = protocolFactory ?? (static () => new JsonLineDebugProtocol());
}
public bool IsListening => _listener is not null;
public IPEndPoint? Endpoint { get; private set; }
public void Start()
{
if (_listener is not null)
{
return;
}
var listener = new TcpListener(_options.BindAddress, _options.Port);
listener.Start(_options.MaxClients);
_listener = listener;
Endpoint = (IPEndPoint?)listener.LocalEndpoint;
Log.Info($"Debug server listening on {Endpoint} (protocol {_protocolFactory().Name})");
_acceptLoop = Task.Run(() => AcceptLoopAsync(listener, _shutdown.Token));
}
private async Task AcceptLoopAsync(TcpListener listener, CancellationToken cancellationToken)
{
while (!cancellationToken.IsCancellationRequested)
{
TcpClient client;
try
{
client = await listener.AcceptTcpClientAsync(cancellationToken).ConfigureAwait(false);
}
catch (OperationCanceledException)
{
break;
}
catch (SocketException) when (cancellationToken.IsCancellationRequested)
{
break;
}
catch (ObjectDisposedException)
{
break;
}
var connection = new DebuggerClientConnection(client, _session, _protocolFactory());
var task = Task.Run(() => ServeAsync(connection, cancellationToken), cancellationToken);
_connections[connection] = task;
}
}
private async Task ServeAsync(DebuggerClientConnection connection, CancellationToken cancellationToken)
{
try
{
await connection.RunAsync(cancellationToken).ConfigureAwait(false);
}
finally
{
_connections.TryRemove(connection, out _);
await connection.DisposeAsync().ConfigureAwait(false);
}
}
public async Task StopAsync()
{
if (_listener is null)
{
return;
}
await _shutdown.CancelAsync().ConfigureAwait(false);
_listener.Stop();
_listener = null;
try
{
if (_acceptLoop is not null)
{
await _acceptLoop.ConfigureAwait(false);
}
await Task.WhenAll(_connections.Values).ConfigureAwait(false);
}
catch (Exception ex) when (ex is OperationCanceledException or SocketException or ObjectDisposedException)
{
// Expected while tearing connections down.
}
_connections.Clear();
Log.Info("Debug server stopped.");
}
public async ValueTask DisposeAsync()
{
await StopAsync().ConfigureAwait(false);
_shutdown.Dispose();
}
}
@@ -1,78 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Net;
namespace SharpEmu.Debugger.Server;
/// <summary>Network configuration for a <see cref="DebuggerServer"/>.</summary>
public sealed class DebuggerServerOptions
{
/// <summary>The default TCP port the debug server listens on.</summary>
public const int DefaultPort = 5714;
/// <summary>
/// The address to bind. Defaults to loopback so the debug surface is not
/// exposed off-box; a caller must opt in to a routable address explicitly.
/// </summary>
public IPAddress BindAddress { get; init; } = IPAddress.Loopback;
/// <summary>The TCP port to listen on.</summary>
public int Port { get; init; } = DefaultPort;
/// <summary>
/// The maximum number of simultaneous client connections. Additional
/// connections wait in the accept backlog.
/// </summary>
public int MaxClients { get; init; } = 4;
/// <summary>
/// Parses a <c>host:port</c>, bare <c>port</c>, or bare host into options.
/// Returns false when the text cannot be interpreted.
/// </summary>
public static bool TryParseEndpoint(string? text, out DebuggerServerOptions options, out string error)
{
options = new DebuggerServerOptions();
error = string.Empty;
if (string.IsNullOrWhiteSpace(text))
{
return true;
}
var value = text.Trim();
var host = value;
var port = DefaultPort;
var separator = value.LastIndexOf(':');
if (separator >= 0)
{
var portText = value[(separator + 1)..];
if (portText.Length > 0)
{
if (!int.TryParse(portText, out port) || port is <= 0 or > 65535)
{
error = $"Invalid port '{portText}'.";
return false;
}
}
host = value[..separator];
}
var address = IPAddress.Loopback;
if (!string.IsNullOrWhiteSpace(host) &&
!string.Equals(host, "localhost", StringComparison.OrdinalIgnoreCase) &&
!IPAddress.TryParse(host, out address!))
{
error = $"Invalid bind address '{host}'.";
return false;
}
options = new DebuggerServerOptions
{
BindAddress = address ?? IPAddress.Loopback,
Port = port,
};
return true;
}
}
@@ -1,24 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Net;
namespace SharpEmu.Debugger.Server;
/// <summary>
/// A network front-end that exposes a debugger session to remote clients.
/// </summary>
public interface IDebuggerServer : IAsyncDisposable
{
/// <summary>True once the listener is accepting connections.</summary>
bool IsListening { get; }
/// <summary>The endpoint the server is bound to, or null before start.</summary>
IPEndPoint? Endpoint { get; }
/// <summary>Binds and begins accepting client connections.</summary>
void Start();
/// <summary>Stops accepting connections and closes active clients.</summary>
Task StopAsync();
}
@@ -1,69 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.Debugger.Breakpoints;
using SharpEmu.HLE;
namespace SharpEmu.Debugger.Session;
/// <summary>
/// Describes a stop delivered to debugger clients: why the target stopped,
/// where, and the register snapshot at that point.
/// </summary>
public sealed class DebugStopEvent
{
public DebugStopEvent(
DebugStopReason reason,
DebugRegisterFile registers,
CpuDebugFrameKind frameKind,
string frameLabel,
Breakpoint? breakpoint = null,
OrbisGen2Result? result = null,
string? detail = null,
string? opcodeBytes = null,
CpuStallInfo? stallInfo = null)
{
Reason = reason;
Registers = registers;
FrameKind = frameKind;
FrameLabel = frameLabel ?? string.Empty;
Breakpoint = breakpoint;
Result = result;
Detail = detail;
OpcodeBytes = opcodeBytes;
StallInfo = stallInfo;
}
public DebugStopReason Reason { get; }
/// <summary>The instruction pointer where the target stopped.</summary>
public ulong Address => Registers.Rip;
public DebugRegisterFile Registers { get; }
public CpuDebugFrameKind FrameKind { get; }
public string FrameLabel { get; }
/// <summary>The breakpoint responsible for the stop, when applicable.</summary>
public Breakpoint? Breakpoint { get; }
/// <summary>
/// The frame result for a <see cref="DebugStopReason.Fault"/> stop; null for
/// non-fault stops.
/// </summary>
public OrbisGen2Result? Result { get; }
/// <summary>A human-readable summary of a fault, when applicable.</summary>
public string? Detail { get; }
/// <summary>
/// A hex preview of the bytes at <see cref="Address"/> (the faulting
/// instruction), when the stop is a fault and the bytes were readable.
/// </summary>
public string? OpcodeBytes { get; }
/// <summary>Structured backend evidence for a stall stop.</summary>
public CpuStallInfo? StallInfo { get; }
}
@@ -1,32 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Debugger.Session;
/// <summary>Why the target stopped and handed control to the debugger.</summary>
public enum DebugStopReason
{
/// <summary>Stopped at the configured entry point before running any frame.</summary>
EntryPoint,
/// <summary>An execution breakpoint was hit.</summary>
Breakpoint,
/// <summary>A data watchpoint was hit.</summary>
Watchpoint,
/// <summary>A single-step (frame step) request completed.</summary>
Step,
/// <summary>A client-requested pause took effect.</summary>
Pause,
/// <summary>The guest raised a fault or trap.</summary>
Fault,
/// <summary>
/// The backend detected an execution stall (for example a mutex spin loop /
/// livelock) with no forward progress.
/// </summary>
Stall,
}
@@ -1,23 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Debugger.Session;
/// <summary>The execution state of a debugged target as seen by the debugger.</summary>
public enum DebuggerRunState
{
/// <summary>No guest frame has entered the debugger yet.</summary>
Detached,
/// <summary>The guest is executing and cannot be inspected safely.</summary>
Running,
/// <summary>
/// The guest is parked at a frame boundary. Registers and memory can be
/// read and written, and breakpoints can be edited.
/// </summary>
Paused,
/// <summary>The guest has finished; no further frames will run.</summary>
Terminated,
}
@@ -1,425 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.Debugger.Breakpoints;
using SharpEmu.HLE;
using SharpEmu.Logging;
namespace SharpEmu.Debugger.Session;
/// <summary>
/// The default <see cref="IDebuggerSession"/>. It plugs into the CPU dispatcher
/// as an <see cref="ICpuDebugHook"/>: when a frame boundary warrants a stop it
/// parks the emulation thread inside <see cref="ICpuDebugHook.OnFrameEnter"/>
/// while a debug client inspects and edits state, then releases it on
/// continue/step.
/// </summary>
/// <remarks>
/// Pausing works by blocking the emulation thread on <see cref="_resumeGate"/>
/// from within the hook call. Because that thread is the one that owns the guest
/// context, register and memory accessors are safe to serve from other threads
/// only while it is parked — which is exactly the <see cref="DebuggerRunState.Paused"/>
/// window the accessors gate on.
/// </remarks>
public sealed class DebuggerSession : IDebuggerSession, ICpuDebugHook
{
private static readonly SharpEmuLogger Log = SharpEmuLog.For("SharpEmu.Debugger");
private readonly object _sync = new();
private readonly ManualResetEventSlim _resumeGate = new(initialState: false);
private readonly DebuggerSessionOptions _options;
private ICpuDebugFrame? _currentFrame;
private DebuggerRunState _state = DebuggerRunState.Detached;
private DebugStopEvent? _lastStop;
private bool _seenFirstFrame;
private bool _pausePending;
private bool _stepPending;
public DebuggerSession(DebuggerSessionOptions? options = null)
{
_options = options ?? new DebuggerSessionOptions();
Breakpoints = new BreakpointStore();
}
public BreakpointStore Breakpoints { get; }
public ICpuDebugHook Hook => this;
public event EventHandler<DebugStopEvent>? Stopped;
public event EventHandler? Resumed;
public event EventHandler? Terminated;
public DebuggerRunState State
{
get
{
lock (_sync)
{
return _state;
}
}
}
public DebugStopEvent? LastStop
{
get
{
lock (_sync)
{
return _lastStop;
}
}
}
void ICpuDebugHook.OnFrameEnter(ICpuDebugFrame frame)
{
DebugStopEvent? stop;
lock (_sync)
{
_currentFrame = frame;
var firstFrame = !_seenFirstFrame;
_seenFirstFrame = true;
var reason = ResolveStopReason(frame, firstFrame, out var breakpoint);
if (reason is null)
{
_state = DebuggerRunState.Running;
return;
}
_state = DebuggerRunState.Paused;
_lastStop = new DebugStopEvent(
reason.Value,
DebugRegisterFile.Capture(frame),
frame.Kind,
frame.Label,
breakpoint);
stop = _lastStop;
_resumeGate.Reset();
}
Log.Debug($"Debugger stop: {stop!.Reason} at 0x{stop.Address:X16} ({stop.FrameLabel})");
Stopped?.Invoke(this, stop);
// Park the emulation thread until a client resumes the target. The frame
// stays live and inspectable for the whole wait.
_resumeGate.Wait();
lock (_sync)
{
if (_state != DebuggerRunState.Terminated)
{
_state = DebuggerRunState.Running;
}
}
Resumed?.Invoke(this, EventArgs.Empty);
}
void ICpuDebugHook.OnFrameExit(ICpuDebugFrame frame, OrbisGen2Result result)
{
DebugStopEvent? stop = null;
lock (_sync)
{
if (_options.BreakOnFault &&
result != OrbisGen2Result.ORBIS_GEN2_OK &&
_state != DebuggerRunState.Terminated)
{
// Parking here keeps the post-fault frame inspectable.
_currentFrame = frame;
_state = DebuggerRunState.Paused;
_lastStop = BuildFaultStop(frame, result);
stop = _lastStop;
_resumeGate.Reset();
}
}
if (stop is not null)
{
Log.Debug($"Debugger fault stop: {stop.Result} at 0x{stop.Address:X16} ({stop.FrameLabel})");
Stopped?.Invoke(this, stop);
_resumeGate.Wait();
Resumed?.Invoke(this, EventArgs.Empty);
}
lock (_sync)
{
if (ReferenceEquals(_currentFrame, frame))
{
_currentFrame = null;
}
if (_state != DebuggerRunState.Terminated)
{
_state = DebuggerRunState.Running;
}
}
}
void ICpuDebugHook.OnStall(ICpuDebugFrame frame, CpuStallInfo info)
{
if (!_options.BreakOnStall)
{
return;
}
DebugStopEvent? stop = null;
lock (_sync)
{
if (_state == DebuggerRunState.Terminated)
{
return;
}
_currentFrame = frame;
_state = DebuggerRunState.Paused;
_lastStop = new DebugStopEvent(
DebugStopReason.Stall,
DebugRegisterFile.Capture(frame),
frame.Kind,
frame.Label,
breakpoint: null,
result: null,
detail: info.Detail,
opcodeBytes: ReadOpcodePreview(frame, info.InstructionPointer, 16),
stallInfo: info);
stop = _lastStop;
_resumeGate.Reset();
}
Log.Debug($"Debugger stall stop: {info.Kind} nid={info.Nid} at 0x{info.InstructionPointer:X16}");
Stopped?.Invoke(this, stop);
_resumeGate.Wait();
lock (_sync)
{
if (ReferenceEquals(_currentFrame, frame))
{
_currentFrame = null;
}
if (_state != DebuggerRunState.Terminated)
{
_state = DebuggerRunState.Running;
}
}
Resumed?.Invoke(this, EventArgs.Empty);
}
private static DebugStopEvent BuildFaultStop(ICpuDebugFrame frame, OrbisGen2Result result)
{
var opcodeBytes = ReadOpcodePreview(frame, frame.Rip, 16);
var detail = $"result={result}";
if (opcodeBytes is not null)
{
detail += $", bytes={opcodeBytes}";
}
return new DebugStopEvent(
DebugStopReason.Fault,
DebugRegisterFile.Capture(frame),
frame.Kind,
frame.Label,
breakpoint: null,
result: result,
detail: detail,
opcodeBytes: opcodeBytes);
}
private static string? ReadOpcodePreview(ICpuDebugFrame frame, ulong address, int maxBytes)
{
Span<byte> buffer = stackalloc byte[maxBytes];
var count = 0;
for (; count < maxBytes; count++)
{
if (!frame.Memory.TryRead(address + (ulong)count, buffer.Slice(count, 1)))
{
break;
}
}
return count == 0 ? null : Convert.ToHexString(buffer[..count]);
}
/// <summary>
/// Signals that the whole guest run has finished. Releases any parked
/// emulation thread and moves the session to
/// <see cref="DebuggerRunState.Terminated"/>.
/// </summary>
public void NotifyTerminated()
{
lock (_sync)
{
_state = DebuggerRunState.Terminated;
_currentFrame = null;
}
_resumeGate.Set();
Terminated?.Invoke(this, EventArgs.Empty);
}
private DebugStopReason? ResolveStopReason(ICpuDebugFrame frame, bool firstFrame, out Breakpoint? breakpoint)
{
breakpoint = null;
if (_pausePending)
{
_pausePending = false;
return DebugStopReason.Pause;
}
if (_stepPending)
{
_stepPending = false;
return DebugStopReason.Step;
}
var hit = Breakpoints.FindExecuteHit(frame.EntryPoint);
if (hit is not null)
{
breakpoint = hit;
return DebugStopReason.Breakpoint;
}
if (_options.StopAtEntry && firstFrame)
{
return DebugStopReason.EntryPoint;
}
return null;
}
public bool TryGetRegisters(out DebugRegisterFile registers)
{
lock (_sync)
{
if (!IsPausedWithFrame(out var frame))
{
registers = default;
return false;
}
registers = DebugRegisterFile.Capture(frame);
return true;
}
}
public bool TrySetRegister(DebugRegisterId id, ulong value)
{
lock (_sync)
{
if (!IsPausedWithFrame(out var frame))
{
return false;
}
if (id.IsGeneralPurpose())
{
frame.SetRegister(id.ToCpuRegister(), value);
return true;
}
switch (id)
{
case DebugRegisterId.Rip:
frame.Rip = value;
return true;
case DebugRegisterId.Rflags:
frame.Rflags = value;
return true;
default:
// FS/GS bases are owned by the TLS setup and are read-only here.
return false;
}
}
}
public bool TryReadMemory(ulong address, Span<byte> destination)
{
lock (_sync)
{
return IsPausedWithFrame(out var frame) && frame.Memory.TryRead(address, destination);
}
}
public bool TryWriteMemory(ulong address, ReadOnlySpan<byte> source)
{
lock (_sync)
{
return IsPausedWithFrame(out var frame) && frame.Memory.TryWrite(address, source);
}
}
public bool TryReadXmm(int registerIndex, out ulong low, out ulong high)
{
lock (_sync)
{
if (!IsPausedWithFrame(out var frame) || (uint)registerIndex >= 16)
{
low = 0;
high = 0;
return false;
}
frame.GetXmm(registerIndex, out low, out high);
return true;
}
}
public bool Continue()
{
lock (_sync)
{
if (_state != DebuggerRunState.Paused)
{
return false;
}
_resumeGate.Set();
return true;
}
}
public bool StepFrame()
{
lock (_sync)
{
if (_state != DebuggerRunState.Paused)
{
return false;
}
_stepPending = true;
_resumeGate.Set();
return true;
}
}
public void RequestPause()
{
lock (_sync)
{
if (_state == DebuggerRunState.Running)
{
_pausePending = true;
}
}
}
private bool IsPausedWithFrame(out ICpuDebugFrame frame)
{
// Callers must hold _sync.
if (_state == DebuggerRunState.Paused && _currentFrame is not null)
{
frame = _currentFrame;
return true;
}
frame = null!;
return false;
}
}
@@ -1,31 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Debugger.Session;
/// <summary>Configuration for a <see cref="DebuggerSession"/>.</summary>
public sealed class DebuggerSessionOptions
{
/// <summary>
/// When true, the session pauses at the first frame it observes so a client
/// can attach breakpoints before the guest runs. Defaults to true, matching
/// the "stop at entry" behaviour most debuggers expose.
/// </summary>
public bool StopAtEntry { get; init; } = true;
/// <summary>
/// When true, the session pauses when a frame ends with a non-OK result (a
/// CPU trap, memory fault, or unimplemented path) so a client can inspect the
/// post-fault register/memory state before the frame is torn down. Defaults
/// to true. The stop reports <see cref="DebugStopReason.Fault"/>.
/// </summary>
public bool BreakOnFault { get; init; } = true;
/// <summary>
/// When true, the session pauses when the backend detects an execution stall
/// (a mutex spin loop / livelock) before the guest is forced out of the loop,
/// so a client can inspect the stalled state. Defaults to true. The stop
/// reports <see cref="DebugStopReason.Stall"/>.
/// </summary>
public bool BreakOnStall { get; init; } = true;
}
@@ -1,51 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.Debugger.Session;
/// <summary>
/// The inspection and control surface a debugger front-end (for example a
/// network server) drives. Register and memory accessors succeed only while the
/// target is <see cref="DebuggerRunState.Paused"/>; they return <c>false</c>
/// otherwise so callers never read torn state from a running guest.
/// </summary>
public interface IDebugTarget
{
/// <summary>The current execution state.</summary>
DebuggerRunState State { get; }
/// <summary>The most recent stop, or null if the target has not stopped yet.</summary>
DebugStopEvent? LastStop { get; }
/// <summary>Reads the integer register file. Fails unless paused.</summary>
bool TryGetRegisters(out DebugRegisterFile registers);
/// <summary>Writes a single register. Fails unless paused.</summary>
bool TrySetRegister(DebugRegisterId id, ulong value);
/// <summary>Reads guest memory into <paramref name="destination"/>. Fails unless paused.</summary>
bool TryReadMemory(ulong address, Span<byte> destination);
/// <summary>Writes guest memory from <paramref name="source"/>. Fails unless paused.</summary>
bool TryWriteMemory(ulong address, ReadOnlySpan<byte> source);
/// <summary>Reads a 128-bit XMM register. Fails unless paused.</summary>
bool TryReadXmm(int registerIndex, out ulong low, out ulong high);
/// <summary>
/// Resumes a paused target. Returns false when the target was not paused.
/// </summary>
bool Continue();
/// <summary>
/// Resumes a paused target and stops again at the next frame boundary.
/// Returns false when the target was not paused.
/// </summary>
bool StepFrame();
/// <summary>
/// Requests that a running target stop at the next frame boundary. Has no
/// effect if the target is already paused or terminated.
/// </summary>
void RequestPause();
}
@@ -1,43 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Core.Cpu.Debugging;
using SharpEmu.Debugger.Breakpoints;
namespace SharpEmu.Debugger.Session;
/// <summary>
/// The debugger's coordination point. It bridges the CPU dispatcher seam
/// (<see cref="Hook"/>) to the inspection surface (<see cref="IDebugTarget"/>),
/// owns breakpoint state, and raises lifecycle events that a server relays to
/// connected clients.
/// </summary>
public interface IDebuggerSession : IDebugTarget
{
/// <summary>The breakpoints armed for this session.</summary>
BreakpointStore Breakpoints { get; }
/// <summary>
/// The dispatcher-facing hook. Assign this to
/// <c>SharpEmuRuntimeOptions.DebugHook</c> so guest frames are routed through
/// the session.
/// </summary>
ICpuDebugHook Hook { get; }
/// <summary>Raised on the emulation thread each time the target stops.</summary>
event EventHandler<DebugStopEvent>? Stopped;
/// <summary>Raised when a paused target resumes.</summary>
event EventHandler? Resumed;
/// <summary>Raised once the target has terminated.</summary>
event EventHandler? Terminated;
/// <summary>
/// Signals that the guest run has finished. Releases any parked emulation
/// thread and transitions the session to
/// <see cref="DebuggerRunState.Terminated"/>. Hosts call this after the
/// runtime returns.
/// </summary>
void NotifyTerminated();
}
@@ -1,16 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Project Sdk="Microsoft.NET.Sdk">
<ItemGroup>
<ProjectReference Include="..\SharpEmu.Core\SharpEmu.Core.csproj" />
<ProjectReference Include="..\SharpEmu.HLE\SharpEmu.HLE.csproj" />
<ProjectReference Include="..\SharpEmu.Logging\SharpEmu.Logging.csproj" />
</ItemGroup>
<PropertyGroup>
<NoWarn>$(NoWarn);1591</NoWarn>
</PropertyGroup>
</Project>
-24
View File
@@ -5,7 +5,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Application xmlns="https://github.com/avaloniaui" <Application xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SharpEmu.GUI"
x:Class="SharpEmu.GUI.App" x:Class="SharpEmu.GUI.App"
RequestedThemeVariant="Dark"> RequestedThemeVariant="Dark">
@@ -33,29 +32,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<SolidColorBrush x:Key="InfoBrush" Color="#58A6FF" /> <SolidColorBrush x:Key="InfoBrush" Color="#58A6FF" />
<SolidColorBrush x:Key="TileHoverBrush" Color="#1A2130" /> <SolidColorBrush x:Key="TileHoverBrush" Color="#1A2130" />
<SolidColorBrush x:Key="TileSelectedBrush" Color="#212A3F" /> <SolidColorBrush x:Key="TileSelectedBrush" Color="#212A3F" />
<ControlTheme x:Key="{x:Type local:SettingRow}" TargetType="local:SettingRow">
<Setter Property="Template">
<ControlTemplate>
<Grid ColumnDefinitions="*,Auto">
<StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<TextBlock x:Name="PART_Label" Text="{TemplateBinding Label}" FontSize="13" />
<TextBlock Text="{TemplateBinding Description}" FontSize="11"
Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap"
IsVisible="{Binding Description, RelativeSource={RelativeSource TemplatedParent},
Converter={x:Static StringConverters.IsNotNullOrEmpty}}" />
</StackPanel>
<StackPanel Grid.Column="1" Orientation="Horizontal" Spacing="10" VerticalAlignment="Center">
<ToggleSwitch OnContent="Override" OffContent="Override" MinWidth="0"
VerticalAlignment="Center"
IsVisible="{TemplateBinding ShowOverride}"
IsChecked="{Binding IsOverridden, RelativeSource={RelativeSource TemplatedParent}, Mode=TwoWay}" />
<ContentPresenter x:Name="PART_Slot" Content="{TemplateBinding Content}" VerticalAlignment="Center" />
</StackPanel>
</Grid>
</ControlTemplate>
</Setter>
</ControlTheme>
</Application.Resources> </Application.Resources>
<Application.Styles> <Application.Styles>
+2 -45
View File
@@ -125,48 +125,5 @@
"Dialog.PsExecutables": "ملفات PS التنفيذية", "Dialog.PsExecutables": "ملفات PS التنفيذية",
"Dialog.SaveLogFile": "حدد مكان حفظ ملف السجل", "Dialog.SaveLogFile": "حدد مكان حفظ ملف السجل",
"Dialog.PlainTextFiles": "ملفات نصية عادية", "Dialog.PlainTextFiles": "ملفات نصية عادية",
"Dialog.LogFiles": "ملفات السجل", "Dialog.LogFiles": "ملفات السجل"
}
"Library.Context.GameSettings": "إعدادات اللعبة…",
"Options.Env.Tab": "البيئة",
"Options.Section.Environment": "متغيرات البيئة",
"Options.Env.Desc": "خيارات تُمرر إلى المحاكي كمتغيرات بيئة عند التشغيل.",
"Options.Env.Bthid.Desc": "الإبلاغ عن أن Bluetooth HID غير متاح للألعاب التي تنتظر برمجيات عجلة القيادة/FFB فيها إلى ما لا نهاية.\nاتركه معطلاً عادةً. بعض الألعاب تتجمد عند فشل التهيئة.",
"Options.Env.LoopGuard.Desc": "عدم إجبار الألعاب التي تكرر النداء نفسه لفترة طويلة على الإغلاق.\nجرّب هذا عندما تُغلق لعبة نفسها أثناء التحميل.",
"Options.Env.WritableApp0.Desc": "السماح للألعاب بإنشاء الملفات والكتابة داخل مجلد التثبيت الخاص بها.\nمطلوب للنسخ غير المحزومة التي تكتب بيانات الحفظ أو الإعدادات تحت ‎/app0.",
"Options.Env.VkValidation.Desc": "تفعيل طبقات التحقق في Vulkan لتصحيح أخطاء وحدة معالجة الرسوميات.\nبطيء. يتطلب تثبيت Vulkan SDK.",
"Options.Env.DumpSpirv.Desc": "تفريغ شيدرات AGC وترجماتها إلى SPIR-V في مجلد shader-dumps.\nاستخدمه عند الإبلاغ عن أخطاء الشيدرات أو العرض.",
"Options.Env.LogDirectMemory.Desc": "تسجيل تخصيصات الذاكرة المباشرة وإخفاقاتها في وحدة التحكم.\nاستخدمه عندما تنهار لعبة أو تُغلق أثناء الإقلاع.",
"Options.Env.LogIo.Desc": "تسجيل فتح الملفات وقراءتها وحلّ المسارات في وحدة التحكم.\nاستخدمه عندما لا تجد لعبة ملفات بياناتها أثناء الإقلاع.",
"Options.Env.LogNp.Desc": "تسجيل نداءات مكتبة NP (شبكة PlayStation) في وحدة التحكم.",
"Common.Save": "حفظ",
"Common.Cancel": "إلغاء",
"PerGame.Title": "إعدادات خاصة باللعبة — {0} ({1})",
"PerGame.InheritNote": "الصفوف غير المحددة ترث الإعدادات الافتراضية العامة.",
"PerGame.EnvToggles.Label": "مفاتيح البيئة",
"PerGame.EnvToggles.Desc": "تجاوز المجموعة العامة من مفاتيح ‎SHARPEMU_*‎ لهذه اللعبة.",
"Options.About": "حول",
"About.Github.Label": "GitHub",
"About.Github.Desc": "الكود المصدري والمشكلات وتطوير المشروع.",
"About.Github.LatestCommitLabel": "أحدث Commit",
"About.Github.LatestCommitDescription": "أحدث commit على الفرع main",
"About.Discord.Label": "دسكورد",
"About.Discord.Desc": "انضم إلى المجتمع واحصل على الدعم وتابع التطوير.",
"About.GithubButton": "ساهم على GitHub!",
"About.DiscordButton": "انضم إلى دسكوردنا!",
"Updater.Auto.Label": "التحقق من التحديثات عند بدء التشغيل",
"Updater.Auto.Desc": "يتحقق من GitHub دون تأخير بدء التشغيل.",
"Updater.Label": "التحديثات",
"Updater.Check": "التحقق من التحديثات",
"Updater.DownloadRestart": "تنزيل وإعادة التشغيل",
"Updater.Status.Ready": "الإصدار الحالي: {0}",
"Updater.Status.Checking": "جارٍ التحقق من التحديثات…",
"Updater.Status.Current": "أنت على أحدث إصدار ({0}).",
"Updater.Status.Available": "يتوفر إصدار جديد: {0}",
"Updater.Status.Downloading": "جارٍ تنزيل التحديث… {0}%",
"Updater.Status.Installing": "جارٍ تثبيت التحديث…",
"Updater.Status.Timeout": "انتهت مهلة التحقق من التحديثات بعد 10 ثوانٍ.",
"Updater.Status.Failed": "تعذر التحقق من التحديثات.",
"Updater.Status.ChecksumFailed": "فشل التحديث المنزَّل في اجتياز تحقق SHA-256.",
"Updater.Status.Unsupported": "يتطلب التحديث التلقائي إصدار x64 لنظام Windows أو Linux أو macOS."
}
+1 -28
View File
@@ -142,32 +142,5 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Participe da comunidade, obtenha suporte e acompanhe o desenvolvimento.", "About.Discord.Desc": "Participe da comunidade, obtenha suporte e acompanhe o desenvolvimento.",
"About.GithubButton": "Contribua no GitHub!", "About.GithubButton": "Contribua no GitHub!",
"About.DiscordButton": "Entre no nosso Discord!", "About.DiscordButton": "Entre no nosso Discord!"
"Library.Context.GameSettings": "Configurações do jogo…",
"Options.Env.WritableApp0.Desc": "Permite que os jogos criem e gravem arquivos dentro da própria pasta de instalação.\nNecessário para dumps não empacotados que gravam seus saves ou configurações em /app0.",
"Options.Env.LogIo.Desc": "Registra no console a abertura e leitura de arquivos e a resolução de caminhos.\nUse quando um jogo não encontrar seus arquivos de dados durante a inicialização.",
"Common.Save": "Salvar",
"Common.Cancel": "Cancelar",
"PerGame.Title": "Configurações por jogo — {0} ({1})",
"PerGame.InheritNote": "As linhas desmarcadas herdam os padrões globais.",
"PerGame.EnvToggles.Label": "Variáveis de ambiente",
"PerGame.EnvToggles.Desc": "Substitui o conjunto global de opções SHARPEMU_* para este jogo.",
"About.Github.LatestCommitLabel": "Último commit",
"About.Github.LatestCommitDescription": "Último commit na branch main",
"Updater.Auto.Label": "Verificar atualizações ao iniciar",
"Updater.Auto.Desc": "Consulta o GitHub sem atrasar a inicialização.",
"Updater.Label": "Atualizações",
"Updater.Check": "Verificar atualizações",
"Updater.DownloadRestart": "Baixar e reiniciar",
"Updater.Status.Ready": "Build atual: {0}",
"Updater.Status.Checking": "Verificando atualizações…",
"Updater.Status.Current": "Você está atualizado ({0}).",
"Updater.Status.Available": "Um novo build está disponível: {0}",
"Updater.Status.Downloading": "Baixando atualização… {0}%",
"Updater.Status.Installing": "Instalando atualização…",
"Updater.Status.Timeout": "A verificação de atualizações expirou após 10 segundos.",
"Updater.Status.Failed": "Não foi possível verificar as atualizações.",
"Updater.Status.ChecksumFailed": "A atualização baixada falhou na verificação SHA-256.",
"Updater.Status.Unsupported": "A atualização automática requer um build x64 para Windows, Linux ou macOS."
} }
+1 -44
View File
@@ -125,48 +125,5 @@
"Dialog.PsExecutables": "PS-Ausführbare Dateien", "Dialog.PsExecutables": "PS-Ausführbare Dateien",
"Dialog.SaveLogFile": "Protokolldatei speichern unter", "Dialog.SaveLogFile": "Protokolldatei speichern unter",
"Dialog.PlainTextFiles": "Textdateien", "Dialog.PlainTextFiles": "Textdateien",
"Dialog.LogFiles": "Protokolldateien", "Dialog.LogFiles": "Protokolldateien"
"Library.Context.GameSettings": "Spieleinstellungen…",
"Options.Env.Tab": "Umgebung",
"Options.Section.Environment": "UMGEBUNGSVARIABLEN",
"Options.Env.Desc": "Schalter, die dem Emulator beim Start als Umgebungsvariablen übergeben werden.",
"Options.Env.Bthid.Desc": "Bluetooth-HID als nicht verfügbar melden, wenn die Lenkrad-/FFB-Middleware eines Titels endlos wartet.\nNormalerweise ausgeschaltet lassen. Manche Titel frieren ein, wenn die Initialisierung fehlschlägt.",
"Options.Env.LoopGuard.Desc": "Titel nicht zwangsweise beenden, wenn sie denselben Aufruf zu lange wiederholen.\nAusprobieren, wenn ein Spiel sich beim Laden von selbst beendet.",
"Options.Env.WritableApp0.Desc": "Titeln erlauben, Dateien in ihrem Installationsordner anzulegen und zu schreiben.\nNötig für entpackte Dumps, die ihre Spielstände oder Konfiguration unter /app0 speichern.",
"Options.Env.VkValidation.Desc": "Vulkan-Validierungsschichten für GPU-Debugging aktivieren.\nLangsam. Erfordert ein installiertes Vulkan SDK.",
"Options.Env.DumpSpirv.Desc": "AGC-Shader und ihre SPIR-V-Übersetzungen im Ordner shader-dumps ablegen.\nBeim Melden von Shader- oder Grafikfehlern verwenden.",
"Options.Env.LogDirectMemory.Desc": "Direkte Speicherzuweisungen und Fehler in der Konsole protokollieren.\nVerwenden, wenn ein Spiel beim Start abbricht oder sich beendet.",
"Options.Env.LogIo.Desc": "Datei-Öffnen, -Lesen und Pfadauflösung in der Konsole protokollieren.\nVerwenden, wenn ein Spiel beim Start seine Datendateien nicht findet.",
"Options.Env.LogNp.Desc": "NP-Bibliotheksaufrufe (PlayStation Network) in der Konsole protokollieren.",
"Common.Save": "Speichern",
"Common.Cancel": "Abbrechen",
"PerGame.Title": "Spielspezifische Einstellungen — {0} ({1})",
"PerGame.InheritNote": "Nicht angehakte Zeilen übernehmen die globalen Standardwerte.",
"PerGame.EnvToggles.Label": "Umgebungsschalter",
"PerGame.EnvToggles.Desc": "Die globalen SHARPEMU_*-Schalter für dieses Spiel überschreiben.",
"Options.About": "Über",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Quellcode, Issues und Projektentwicklung.",
"About.Github.LatestCommitLabel": "Neuester Commit",
"About.Github.LatestCommitDescription": "Neuester Commit auf dem main-Branch",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Tritt der Community bei, erhalte Support und verfolge die Entwicklung.",
"About.GithubButton": "Auf GitHub mitwirken!",
"About.DiscordButton": "Tritt unserem Discord bei!",
"Updater.Auto.Label": "Beim Start nach Updates suchen",
"Updater.Auto.Desc": "Fragt GitHub ab, ohne den Start zu verzögern.",
"Updater.Label": "Updates",
"Updater.Check": "Nach Updates suchen",
"Updater.DownloadRestart": "Herunterladen und neu starten",
"Updater.Status.Ready": "Aktueller Build: {0}",
"Updater.Status.Checking": "Suche nach Updates…",
"Updater.Status.Current": "Du bist auf dem neuesten Stand ({0}).",
"Updater.Status.Available": "Ein neuer Build ist verfügbar: {0}",
"Updater.Status.Downloading": "Update wird heruntergeladen… {0}%",
"Updater.Status.Installing": "Update wird installiert…",
"Updater.Status.Timeout": "Die Updateprüfung ist nach 10 Sekunden abgelaufen.",
"Updater.Status.Failed": "Updates konnten nicht geprüft werden.",
"Updater.Status.ChecksumFailed": "Das heruntergeladene Update hat die SHA-256-Prüfung nicht bestanden.",
"Updater.Status.Unsupported": "Automatische Updates erfordern einen x64-Build für Windows, Linux oder macOS."
} }
+1 -44
View File
@@ -125,48 +125,5 @@
"Dialog.PsExecutables": "PS-programmer", "Dialog.PsExecutables": "PS-programmer",
"Dialog.SaveLogFile": "Vælg hvor logfilen skal gemmes", "Dialog.SaveLogFile": "Vælg hvor logfilen skal gemmes",
"Dialog.PlainTextFiles": "Almindelige tekstfiler", "Dialog.PlainTextFiles": "Almindelige tekstfiler",
"Dialog.LogFiles": "Logfiler", "Dialog.LogFiles": "Logfiler"
"Library.Context.GameSettings": "Spilindstillinger…",
"Options.Env.Tab": "Miljø",
"Options.Section.Environment": "MILJØVARIABLER",
"Options.Env.Desc": "Kontakter, der gives videre til emulatoren som miljøvariabler ved start.",
"Options.Env.Bthid.Desc": "Rapportér Bluetooth HID som utilgængelig for titler, hvis rat-/FFB-middleware venter i det uendelige.\nLad den normalt være slået fra. Nogle titler fryser, når initialiseringen fejler.",
"Options.Env.LoopGuard.Desc": "Tving ikke titler til at lukke, når de gentager det samme kald for længe.\nPrøv dette, når et spil lukker af sig selv under indlæsning.",
"Options.Env.WritableApp0.Desc": "Tillad titler at oprette og skrive filer i deres installationsmappe.\nKræves af upakkede dumps, der skriver deres gemte data eller konfiguration under /app0.",
"Options.Env.VkValidation.Desc": "Aktivér Vulkan-valideringslag til GPU-fejlfinding.\nLangsomt. Kræver at Vulkan SDK er installeret.",
"Options.Env.DumpSpirv.Desc": "Gem AGC-shadere og deres SPIR-V-oversættelser i mappen shader-dumps.\nBrug dette, når du rapporterer shader- eller grafikfejl.",
"Options.Env.LogDirectMemory.Desc": "Log direkte hukommelsestildelinger og fejl til konsollen.\nBrug dette, når et spil afbryder eller lukker under opstart.",
"Options.Env.LogIo.Desc": "Log åbning og læsning af filer samt stiopslag til konsollen.\nBrug dette, når et spil ikke kan finde sine datafiler under opstart.",
"Options.Env.LogNp.Desc": "Log NP-bibliotekskald (PlayStation Network) til konsollen.",
"Common.Save": "Gem",
"Common.Cancel": "Annuller",
"PerGame.Title": "Indstillinger pr. spil — {0} ({1})",
"PerGame.InheritNote": "Umarkerede rækker arver de globale standardværdier.",
"PerGame.EnvToggles.Label": "Miljøkontakter",
"PerGame.EnvToggles.Desc": "Tilsidesæt det globale sæt SHARPEMU_*-kontakter for dette spil.",
"Options.About": "Om",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Kildekode, issues og projektudvikling.",
"About.Github.LatestCommitLabel": "Seneste commit",
"About.Github.LatestCommitDescription": "Seneste commit på main-branchen",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Bliv en del af fællesskabet, få hjælp og følg udviklingen.",
"About.GithubButton": "Bidrag på GitHub!",
"About.DiscordButton": "Bliv medlem af vores Discord!",
"Updater.Auto.Label": "Søg efter opdateringer ved start",
"Updater.Auto.Desc": "Tjekker GitHub uden at forsinke opstarten.",
"Updater.Label": "Opdateringer",
"Updater.Check": "Søg efter opdateringer",
"Updater.DownloadRestart": "Download og genstart",
"Updater.Status.Ready": "Nuværende build: {0}",
"Updater.Status.Checking": "Søger efter opdateringer…",
"Updater.Status.Current": "Du er opdateret ({0}).",
"Updater.Status.Available": "Et nyt build er tilgængeligt: {0}",
"Updater.Status.Downloading": "Downloader opdatering… {0}%",
"Updater.Status.Installing": "Installerer opdatering…",
"Updater.Status.Timeout": "Opdateringstjekket fik timeout efter 10 sekunder.",
"Updater.Status.Failed": "Kunne ikke søge efter opdateringer.",
"Updater.Status.ChecksumFailed": "Den downloadede opdatering bestod ikke SHA-256-verifikationen.",
"Updater.Status.Unsupported": "Automatisk opdatering kræver et x64-build til Windows, Linux eller macOS."
} }
-9
View File
@@ -15,7 +15,6 @@
"Library.Context.OpenFolder": "Open game folder", "Library.Context.OpenFolder": "Open game folder",
"Library.Context.CopyPath": "Copy path", "Library.Context.CopyPath": "Copy path",
"Library.Context.CopyTitleId": "Copy title ID", "Library.Context.CopyTitleId": "Copy title ID",
"Library.Context.GameSettings": "Game settings…",
"Library.Context.Remove": "Remove from library", "Library.Context.Remove": "Remove from library",
"Library.Empty.Title": "Your library is empty", "Library.Empty.Title": "Your library is empty",
@@ -82,13 +81,6 @@
"Common.On": "On", "Common.On": "On",
"Common.Off": "Off", "Common.Off": "Off",
"Common.Save": "Save",
"Common.Cancel": "Cancel",
"PerGame.Title": "Per-game settings — {0} ({1})",
"PerGame.InheritNote": "Unchecked rows inherit the global defaults.",
"PerGame.EnvToggles.Label": "Environment toggles",
"PerGame.EnvToggles.Desc": "Override the global set of SHARPEMU_* switches for this game.",
"Console.Title": "CONSOLE", "Console.Title": "CONSOLE",
"Console.SearchWatermark": "Search...", "Console.SearchWatermark": "Search...",
@@ -169,6 +161,5 @@
"Updater.Status.Installing": "Installing update…", "Updater.Status.Installing": "Installing update…",
"Updater.Status.Timeout": "Update check timed out after 10 seconds.", "Updater.Status.Timeout": "Update check timed out after 10 seconds.",
"Updater.Status.Failed": "Could not check for updates.", "Updater.Status.Failed": "Could not check for updates.",
"Updater.Status.ChecksumFailed": "Downloaded update failed SHA-256 verification.",
"Updater.Status.Unsupported": "Automatic updating requires a Windows, Linux or macOS x64 build." "Updater.Status.Unsupported": "Automatic updating requires a Windows, Linux or macOS x64 build."
} }
+1 -35
View File
@@ -135,39 +135,5 @@
"About.Github.LatestCommitDescription": "Último commit en la rama main", "About.Github.LatestCommitDescription": "Último commit en la rama main",
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.", "About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
"About.GithubButton": "Contribuye en GitHub!", "About.GithubButton": "Contribuye en GitHub!",
"About.DiscordButton": "Únete a nuestro Discord!", "About.DiscordButton": "Únete a nuestro Discord!"
"Library.Context.GameSettings": "Ajustes del juego…",
"Options.Env.Tab": "Entorno",
"Options.Section.Environment": "VARIABLES DE ENTORNO",
"Options.Env.Desc": "Opciones que se pasan al emulador como variables de entorno al iniciar.",
"Options.Env.Bthid.Desc": "Indicar que Bluetooth HID no está disponible para títulos cuyo middleware de volante/FFB espera indefinidamente.\nDéjalo desactivado normalmente. Algunos títulos se congelan cuando la inicialización falla.",
"Options.Env.LoopGuard.Desc": "No forzar el cierre de títulos que repiten la misma llamada durante demasiado tiempo.\nPruébalo cuando un juego se cierre solo durante la carga.",
"Options.Env.WritableApp0.Desc": "Permitir que los títulos creen y escriban archivos dentro de su carpeta de instalación.\nNecesario para dumps sin empaquetar que guardan sus datos o configuración en /app0.",
"Options.Env.VkValidation.Desc": "Activar las capas de validación de Vulkan para depurar la GPU.\nLento. Requiere tener instalado el SDK de Vulkan.",
"Options.Env.DumpSpirv.Desc": "Volcar los shaders AGC y sus traducciones SPIR-V a la carpeta shader-dumps.\nÚsalo al informar de errores de shaders o de renderizado.",
"Options.Env.LogDirectMemory.Desc": "Registrar en la consola las asignaciones de memoria directa y sus fallos.\nÚsalo cuando un juego se aborte o se cierre durante el arranque.",
"Options.Env.LogIo.Desc": "Registrar en la consola la apertura y lectura de archivos y la resolución de rutas.\nÚsalo cuando un juego no encuentre sus archivos de datos durante el arranque.",
"Options.Env.LogNp.Desc": "Registrar en la consola las llamadas a la biblioteca NP (PlayStation Network).",
"Common.Save": "Guardar",
"Common.Cancel": "Cancelar",
"PerGame.Title": "Ajustes por juego — {0} ({1})",
"PerGame.InheritNote": "Las filas sin marcar heredan los valores globales.",
"PerGame.EnvToggles.Label": "Variables de entorno",
"PerGame.EnvToggles.Desc": "Sustituir el conjunto global de opciones SHARPEMU_* para este juego.",
"Updater.Auto.Label": "Buscar actualizaciones al iniciar",
"Updater.Auto.Desc": "Consulta GitHub sin retrasar el arranque.",
"Updater.Label": "Actualizaciones",
"Updater.Check": "Buscar actualizaciones",
"Updater.DownloadRestart": "Descargar y reiniciar",
"Updater.Status.Ready": "Build actual: {0}",
"Updater.Status.Checking": "Buscando actualizaciones…",
"Updater.Status.Current": "Estás al día ({0}).",
"Updater.Status.Available": "Hay un nuevo build disponible: {0}",
"Updater.Status.Downloading": "Descargando actualización… {0}%",
"Updater.Status.Installing": "Instalando actualización…",
"Updater.Status.Timeout": "La comprobación de actualizaciones caducó tras 10 segundos.",
"Updater.Status.Failed": "No se pudieron comprobar las actualizaciones.",
"Updater.Status.ChecksumFailed": "La actualización descargada no superó la verificación SHA-256.",
"Updater.Status.Unsupported": "La actualización automática requiere un build x64 de Windows, Linux o macOS."
} }
+1 -28
View File
@@ -142,32 +142,5 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Rejoignez la communauté, obtenez de laide et suivez le développement.", "About.Discord.Desc": "Rejoignez la communauté, obtenez de laide et suivez le développement.",
"About.GithubButton": "Contribuer sur GitHub !", "About.GithubButton": "Contribuer sur GitHub !",
"About.DiscordButton": "Rejoindre notre Discord !", "About.DiscordButton": "Rejoindre notre Discord !"
"Library.Context.GameSettings": "Paramètres du jeu…",
"Options.Env.WritableApp0.Desc": "Autoriser les jeux à créer et écrire des fichiers dans leur dossier dinstallation.\nNécessaire pour les dumps non empaquetés qui écrivent leurs sauvegardes ou leur configuration sous /app0.",
"Options.Env.LogIo.Desc": "Journaliser louverture et la lecture des fichiers ainsi que la résolution des chemins dans la console.\nÀ utiliser quand un jeu ne trouve pas ses fichiers de données au démarrage.",
"Common.Save": "Enregistrer",
"Common.Cancel": "Annuler",
"PerGame.Title": "Paramètres par jeu — {0} ({1})",
"PerGame.InheritNote": "Les lignes non cochées héritent des valeurs globales par défaut.",
"PerGame.EnvToggles.Label": "Variables denvironnement",
"PerGame.EnvToggles.Desc": "Remplacer lensemble global des options SHARPEMU_* pour ce jeu.",
"About.Github.LatestCommitLabel": "Dernier commit",
"About.Github.LatestCommitDescription": "Dernier commit sur la branche main",
"Updater.Auto.Label": "Vérifier les mises à jour au démarrage",
"Updater.Auto.Desc": "Interroge GitHub sans retarder le démarrage.",
"Updater.Label": "Mises à jour",
"Updater.Check": "Vérifier les mises à jour",
"Updater.DownloadRestart": "Télécharger et redémarrer",
"Updater.Status.Ready": "Build actuel : {0}",
"Updater.Status.Checking": "Recherche de mises à jour…",
"Updater.Status.Current": "Vous êtes à jour ({0}).",
"Updater.Status.Available": "Un nouveau build est disponible : {0}",
"Updater.Status.Downloading": "Téléchargement de la mise à jour… {0}%",
"Updater.Status.Installing": "Installation de la mise à jour…",
"Updater.Status.Timeout": "La vérification des mises à jour a expiré après 10 secondes.",
"Updater.Status.Failed": "Impossible de vérifier les mises à jour.",
"Updater.Status.ChecksumFailed": "La mise à jour téléchargée a échoué à la vérification SHA-256.",
"Updater.Status.Unsupported": "La mise à jour automatique nécessite un build x64 pour Windows, Linux ou macOS."
} }
+2 -29
View File
@@ -142,32 +142,5 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Csatlakozz a közösséghe, kérj segítéget és kövesd nyomon a fejlesztést.", "About.Discord.Desc": "Csatlakozz a közösséghe, kérj segítéget és kövesd nyomon a fejlesztést.",
"About.GithubButton": "Járulj hozzá GitHubon!", "About.GithubButton": "Járulj hozzá GitHubon!",
"About.DiscordButton": "Csatlakozz a Discordunhoz!", "About.DiscordButton": "Csatlakozz a Discordunhoz!"
}
"Library.Context.GameSettings": "Játékbeállítások…",
"Options.Env.WritableApp0.Desc": "Engedélyezi, hogy a játékok fájlokat hozzanak létre és írjanak a telepítési mappájukban.\nA kicsomagolt dumpokhoz szükséges, amelyek a mentéseiket vagy beállításaikat az /app0 alá írják.",
"Options.Env.LogIo.Desc": "A fájlmegnyitások, olvasások és útvonal-feloldások naplózása a konzolra.\nAkkor használd, ha egy játék indításkor nem találja az adatfájljait.",
"Common.Save": "Mentés",
"Common.Cancel": "Mégse",
"PerGame.Title": "Játékonkénti beállítások — {0} ({1})",
"PerGame.InheritNote": "A be nem jelölt sorok a globális alapértelmezéseket öröklik.",
"PerGame.EnvToggles.Label": "Környezeti kapcsolók",
"PerGame.EnvToggles.Desc": "A globális SHARPEMU_* kapcsolókészlet felülírása ennél a játéknál.",
"About.Github.LatestCommitLabel": "Legutóbbi commit",
"About.Github.LatestCommitDescription": "A main ág legutóbbi commitja",
"Updater.Auto.Label": "Frissítések keresése indításkor",
"Updater.Auto.Desc": "A GitHubot az indítás késleltetése nélkül ellenőrzi.",
"Updater.Label": "Frissítések",
"Updater.Check": "Frissítések keresése",
"Updater.DownloadRestart": "Letöltés és újraindítás",
"Updater.Status.Ready": "Jelenlegi build: {0}",
"Updater.Status.Checking": "Frissítések keresése…",
"Updater.Status.Current": "Naprakész vagy ({0}).",
"Updater.Status.Available": "Új build érhető el: {0}",
"Updater.Status.Downloading": "Frissítés letöltése… {0}%",
"Updater.Status.Installing": "Frissítés telepítése…",
"Updater.Status.Timeout": "A frissítés-ellenőrzés 10 másodperc után túllépte az időkorlátot.",
"Updater.Status.Failed": "Nem sikerült frissítéseket keresni.",
"Updater.Status.ChecksumFailed": "A letöltött frissítés nem ment át az SHA-256-ellenőrzésen.",
"Updater.Status.Unsupported": "Az automatikus frissítéshez Windows, Linux vagy macOS x64 build szükséges."
}
+1 -44
View File
@@ -130,48 +130,5 @@
"Dialog.PsExecutables": "Eseguibili PS", "Dialog.PsExecutables": "Eseguibili PS",
"Dialog.SaveLogFile": "Scegli dove salvare il file di log", "Dialog.SaveLogFile": "Scegli dove salvare il file di log",
"Dialog.PlainTextFiles": "File di testo semplice", "Dialog.PlainTextFiles": "File di testo semplice",
"Dialog.LogFiles": "File di log", "Dialog.LogFiles": "File di log"
"Library.Context.GameSettings": "Impostazioni del gioco…",
"Options.Env.Tab": "Ambiente",
"Options.Section.Environment": "VARIABILI D'AMBIENTE",
"Options.Env.Desc": "Opzioni passate all'emulatore come variabili d'ambiente all'avvio.",
"Options.Env.Bthid.Desc": "Segnala il Bluetooth HID come non disponibile per i titoli il cui middleware volante/FFB attende all'infinito.\nNormalmente lascialo disattivato. Alcuni titoli si bloccano quando l'inizializzazione fallisce.",
"Options.Env.LoopGuard.Desc": "Non forzare la chiusura dei titoli che ripetono la stessa chiamata troppo a lungo.\nProvalo quando un gioco si chiude da solo durante il caricamento.",
"Options.Env.WritableApp0.Desc": "Consenti ai titoli di creare e scrivere file nella propria cartella di installazione.\nNecessario per i dump non pacchettizzati che scrivono salvataggi o configurazioni in /app0.",
"Options.Env.VkValidation.Desc": "Abilita i validation layer di Vulkan per il debug della GPU.\nLento. Richiede l'SDK di Vulkan installato.",
"Options.Env.DumpSpirv.Desc": "Esporta gli shader AGC e le loro traduzioni SPIR-V nella cartella shader-dumps.\nUsalo quando segnali bug di shader o di rendering.",
"Options.Env.LogDirectMemory.Desc": "Registra in console le allocazioni di memoria diretta e i relativi errori.\nUsalo quando un gioco si interrompe o si chiude durante l'avvio.",
"Options.Env.LogIo.Desc": "Registra in console l'apertura e la lettura dei file e la risoluzione dei percorsi.\nUsalo quando un gioco non trova i propri file di dati durante l'avvio.",
"Options.Env.LogNp.Desc": "Registra in console le chiamate alla libreria NP (PlayStation Network).",
"Common.Save": "Salva",
"Common.Cancel": "Annulla",
"PerGame.Title": "Impostazioni per gioco — {0} ({1})",
"PerGame.InheritNote": "Le righe non selezionate ereditano i valori globali.",
"PerGame.EnvToggles.Label": "Variabili d'ambiente",
"PerGame.EnvToggles.Desc": "Sovrascrivi l'insieme globale delle opzioni SHARPEMU_* per questo gioco.",
"Options.About": "Informazioni",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Codice sorgente, issue e sviluppo del progetto.",
"About.Github.LatestCommitLabel": "Ultimo commit",
"About.Github.LatestCommitDescription": "Ultimo commit sul branch main",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Unisciti alla community, ricevi supporto e segui lo sviluppo.",
"About.GithubButton": "Contribuisci su GitHub!",
"About.DiscordButton": "Unisciti al nostro Discord!",
"Updater.Auto.Label": "Controlla aggiornamenti all'avvio",
"Updater.Auto.Desc": "Interroga GitHub senza rallentare l'avvio.",
"Updater.Label": "Aggiornamenti",
"Updater.Check": "Controlla aggiornamenti",
"Updater.DownloadRestart": "Scarica e riavvia",
"Updater.Status.Ready": "Build attuale: {0}",
"Updater.Status.Checking": "Ricerca aggiornamenti…",
"Updater.Status.Current": "Sei aggiornato ({0}).",
"Updater.Status.Available": "È disponibile un nuovo build: {0}",
"Updater.Status.Downloading": "Download dell'aggiornamento… {0}%",
"Updater.Status.Installing": "Installazione dell'aggiornamento…",
"Updater.Status.Timeout": "Il controllo degli aggiornamenti è scaduto dopo 10 secondi.",
"Updater.Status.Failed": "Impossibile controllare gli aggiornamenti.",
"Updater.Status.ChecksumFailed": "L'aggiornamento scaricato non ha superato la verifica SHA-256.",
"Updater.Status.Unsupported": "L'aggiornamento automatico richiede un build x64 per Windows, Linux o macOS."
} }
+2 -45
View File
@@ -125,48 +125,5 @@
"Dialog.PsExecutables": "PlayStation 実行ファイル", "Dialog.PsExecutables": "PlayStation 実行ファイル",
"Dialog.SaveLogFile": "ログファイルの保存先を選択", "Dialog.SaveLogFile": "ログファイルの保存先を選択",
"Dialog.PlainTextFiles": "プレーンテキストファイル", "Dialog.PlainTextFiles": "プレーンテキストファイル",
"Dialog.LogFiles": "ログファイル", "Dialog.LogFiles": "ログファイル"
}
"Library.Context.GameSettings": "ゲーム設定…",
"Options.Env.Tab": "環境",
"Options.Section.Environment": "環境変数",
"Options.Env.Desc": "起動時に環境変数としてエミュレータへ渡されるスイッチです。",
"Options.Env.Bthid.Desc": "ハンドル/FFBミドルウェアが永久に待機するタイトル向けに、Bluetooth HIDを利用不可として報告します。\n通常はオフのままにしてください。初期化に失敗するとフリーズするタイトルもあります。",
"Options.Env.LoopGuard.Desc": "同じ呼び出しを長時間繰り返すタイトルを強制終了しません。\nロード中にゲームが勝手に終了する場合に試してください。",
"Options.Env.WritableApp0.Desc": "タイトルがインストールフォルダー内にファイルを作成・書き込みできるようにします。\nセーブや設定データを/app0以下に書き込む未パッケージのダンプに必要です。",
"Options.Env.VkValidation.Desc": "GPUデバッグ用のVulkan検証レイヤーを有効にします。\n低速です。Vulkan SDKのインストールが必要です。",
"Options.Env.DumpSpirv.Desc": "AGCシェーダーとそのSPIR-V変換をshader-dumpsフォルダーに出力します。\nシェーダーや描画のバグを報告する際に使用してください。",
"Options.Env.LogDirectMemory.Desc": "ダイレクトメモリの割り当てと失敗をコンソールに記録します。\nゲームが起動中に中断・終了する場合に使用してください。",
"Options.Env.LogIo.Desc": "ファイルのオープン・読み込み・パス解決の動作をコンソールに記録します。\nゲームが起動中にデータファイルを見つけられない場合に使用してください。",
"Options.Env.LogNp.Desc": "NPPlayStation Network)ライブラリの呼び出しをコンソールに記録します。",
"Common.Save": "保存",
"Common.Cancel": "キャンセル",
"PerGame.Title": "ゲームごとの設定 — {0} ({1})",
"PerGame.InheritNote": "チェックされていない行はグローバルの既定値を継承します。",
"PerGame.EnvToggles.Label": "環境スイッチ",
"PerGame.EnvToggles.Desc": "このゲームに対してグローバルのSHARPEMU_*スイッチを上書きします。",
"Options.About": "情報",
"About.Github.Label": "GitHub",
"About.Github.Desc": "ソースコード、Issue、プロジェクトの開発。",
"About.Github.LatestCommitLabel": "最新コミット",
"About.Github.LatestCommitDescription": "mainブランチの最新コミット",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "コミュニティに参加して、サポートを受けたり開発を追いかけたりしましょう。",
"About.GithubButton": "GitHubで貢献しよう!",
"About.DiscordButton": "Discordに参加しよう!",
"Updater.Auto.Label": "起動時にアップデートを確認",
"Updater.Auto.Desc": "起動を遅らせずにGitHubへ確認します。",
"Updater.Label": "アップデート",
"Updater.Check": "アップデートを確認",
"Updater.DownloadRestart": "ダウンロードして再起動",
"Updater.Status.Ready": "現在のビルド: {0}",
"Updater.Status.Checking": "アップデートを確認しています…",
"Updater.Status.Current": "最新の状態です({0})。",
"Updater.Status.Available": "新しいビルドがあります: {0}",
"Updater.Status.Downloading": "アップデートをダウンロード中… {0}%",
"Updater.Status.Installing": "アップデートをインストール中…",
"Updater.Status.Timeout": "アップデートの確認が10秒でタイムアウトしました。",
"Updater.Status.Failed": "アップデートを確認できませんでした。",
"Updater.Status.ChecksumFailed": "ダウンロードしたアップデートはSHA-256検証に失敗しました。",
"Updater.Status.Unsupported": "自動アップデートにはWindows、Linux、またはmacOSのx64ビルドが必要です。"
}
+2 -45
View File
@@ -125,48 +125,5 @@
"Dialog.PsExecutables": "PlayStation 실행 파일", "Dialog.PsExecutables": "PlayStation 실행 파일",
"Dialog.SaveLogFile": "로그 파일 저장 위치 선택", "Dialog.SaveLogFile": "로그 파일 저장 위치 선택",
"Dialog.PlainTextFiles": "일반 텍스트 파일", "Dialog.PlainTextFiles": "일반 텍스트 파일",
"Dialog.LogFiles": "로그 파일", "Dialog.LogFiles": "로그 파일"
}
"Library.Context.GameSettings": "게임 설정…",
"Options.Env.Tab": "환경",
"Options.Section.Environment": "환경 변수",
"Options.Env.Desc": "실행 시 환경 변수로 에뮬레이터에 전달되는 스위치입니다.",
"Options.Env.Bthid.Desc": "휠/FFB 미들웨어가 무한 대기하는 타이틀을 위해 블루투스 HID를 사용 불가로 보고합니다.\n평소에는 꺼 두세요. 초기화에 실패하면 멈추는 타이틀도 있습니다.",
"Options.Env.LoopGuard.Desc": "같은 호출을 너무 오래 반복하는 타이틀을 강제 종료하지 않습니다.\n게임이 로딩 중 저절로 종료될 때 시도해 보세요.",
"Options.Env.WritableApp0.Desc": "타이틀이 설치 폴더 안에 파일을 만들고 쓸 수 있도록 허용합니다.\n세이브나 설정 데이터를 /app0 아래에 쓰는 비패키지 덤프에 필요합니다.",
"Options.Env.VkValidation.Desc": "GPU 디버깅을 위한 Vulkan 검증 레이어를 활성화합니다.\n느립니다. Vulkan SDK가 설치되어 있어야 합니다.",
"Options.Env.DumpSpirv.Desc": "AGC 셰이더와 SPIR-V 변환 결과를 shader-dumps 폴더에 저장합니다.\n셰이더나 렌더링 버그를 보고할 때 사용하세요.",
"Options.Env.LogDirectMemory.Desc": "다이렉트 메모리 할당과 실패를 콘솔에 기록합니다.\n게임이 부팅 중 중단되거나 종료될 때 사용하세요.",
"Options.Env.LogIo.Desc": "파일 열기, 읽기, 경로 확인 동작을 콘솔에 기록합니다.\n게임이 부팅 중 데이터 파일을 찾지 못할 때 사용하세요.",
"Options.Env.LogNp.Desc": "NP(PlayStation Network) 라이브러리 호출을 콘솔에 기록합니다.",
"Common.Save": "저장",
"Common.Cancel": "취소",
"PerGame.Title": "게임별 설정 — {0} ({1})",
"PerGame.InheritNote": "선택하지 않은 항목은 전역 기본값을 따릅니다.",
"PerGame.EnvToggles.Label": "환경 스위치",
"PerGame.EnvToggles.Desc": "이 게임에 대해 전역 SHARPEMU_* 스위치 설정을 재정의합니다.",
"Options.About": "정보",
"About.Github.Label": "GitHub",
"About.Github.Desc": "소스 코드, 이슈, 프로젝트 개발.",
"About.Github.LatestCommitLabel": "최신 커밋",
"About.Github.LatestCommitDescription": "main 브랜치의 최신 커밋",
"About.Discord.Label": "디스코드",
"About.Discord.Desc": "커뮤니티에 참여해 지원을 받고 개발 소식을 확인하세요.",
"About.GithubButton": "GitHub에서 기여하기!",
"About.DiscordButton": "디스코드 참여하기!",
"Updater.Auto.Label": "시작 시 업데이트 확인",
"Updater.Auto.Desc": "시작을 지연시키지 않고 GitHub를 확인합니다.",
"Updater.Label": "업데이트",
"Updater.Check": "업데이트 확인",
"Updater.DownloadRestart": "다운로드 후 재시작",
"Updater.Status.Ready": "현재 빌드: {0}",
"Updater.Status.Checking": "업데이트 확인 중…",
"Updater.Status.Current": "최신 상태입니다 ({0}).",
"Updater.Status.Available": "새 빌드가 있습니다: {0}",
"Updater.Status.Downloading": "업데이트 다운로드 중… {0}%",
"Updater.Status.Installing": "업데이트 설치 중…",
"Updater.Status.Timeout": "업데이트 확인이 10초 후 시간 초과되었습니다.",
"Updater.Status.Failed": "업데이트를 확인할 수 없습니다.",
"Updater.Status.ChecksumFailed": "다운로드한 업데이트가 SHA-256 검증에 실패했습니다.",
"Updater.Status.Unsupported": "자동 업데이트에는 Windows, Linux 또는 macOS x64 빌드가 필요합니다."
}
+1 -44
View File
@@ -125,48 +125,5 @@
"Dialog.PsExecutables": "PS-uitvoerbare bestanden", "Dialog.PsExecutables": "PS-uitvoerbare bestanden",
"Dialog.SaveLogFile": "Selecteer waar het logbestand moet worden opgeslagen", "Dialog.SaveLogFile": "Selecteer waar het logbestand moet worden opgeslagen",
"Dialog.PlainTextFiles": "Platte tekstbestanden", "Dialog.PlainTextFiles": "Platte tekstbestanden",
"Dialog.LogFiles": "Logbestanden", "Dialog.LogFiles": "Logbestanden"
"Library.Context.GameSettings": "Game-instellingen…",
"Options.Env.Tab": "Omgeving",
"Options.Section.Environment": "OMGEVINGSVARIABELEN",
"Options.Env.Desc": "Schakelaars die bij het starten als omgevingsvariabelen aan de emulator worden doorgegeven.",
"Options.Env.Bthid.Desc": "Meld Bluetooth HID als niet beschikbaar voor titels waarvan de stuur-/FFB-middleware eindeloos blijft wachten.\nLaat dit normaal uit. Sommige titels bevriezen wanneer de initialisatie mislukt.",
"Options.Env.LoopGuard.Desc": "Titels die dezelfde aanroep te lang herhalen niet geforceerd afsluiten.\nProbeer dit wanneer een game zichzelf tijdens het laden afsluit.",
"Options.Env.WritableApp0.Desc": "Sta titels toe bestanden aan te maken en te schrijven in hun installatiemap.\nNodig voor uitgepakte dumps die hun save- of configuratiegegevens onder /app0 wegschrijven.",
"Options.Env.VkValidation.Desc": "Schakel Vulkan-validatielagen in voor GPU-debugging.\nTraag. Vereist een geïnstalleerde Vulkan SDK.",
"Options.Env.DumpSpirv.Desc": "Sla AGC-shaders en hun SPIR-V-vertalingen op in de map shader-dumps.\nGebruik dit bij het melden van shader- of renderfouten.",
"Options.Env.LogDirectMemory.Desc": "Log directe geheugentoewijzingen en fouten naar de console.\nGebruik dit wanneer een game tijdens het opstarten afbreekt of afsluit.",
"Options.Env.LogIo.Desc": "Log het openen en lezen van bestanden en het oplossen van paden naar de console.\nGebruik dit wanneer een game zijn databestanden niet kan vinden tijdens het opstarten.",
"Options.Env.LogNp.Desc": "Log NP-bibliotheekaanroepen (PlayStation Network) naar de console.",
"Common.Save": "Opslaan",
"Common.Cancel": "Annuleren",
"PerGame.Title": "Instellingen per game — {0} ({1})",
"PerGame.InheritNote": "Niet-aangevinkte rijen erven de globale standaardwaarden.",
"PerGame.EnvToggles.Label": "Omgevingsschakelaars",
"PerGame.EnvToggles.Desc": "Overschrijf de globale set SHARPEMU_*-schakelaars voor deze game.",
"Options.About": "Over",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Broncode, issues en projectontwikkeling.",
"About.Github.LatestCommitLabel": "Nieuwste commit",
"About.Github.LatestCommitDescription": "Nieuwste commit op de main-branch",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Word lid van de community, krijg ondersteuning en volg de ontwikkeling.",
"About.GithubButton": "Draag bij op GitHub!",
"About.DiscordButton": "Word lid van onze Discord!",
"Updater.Auto.Label": "Bij het opstarten controleren op updates",
"Updater.Auto.Desc": "Controleert GitHub zonder het opstarten te vertragen.",
"Updater.Label": "Updates",
"Updater.Check": "Controleren op updates",
"Updater.DownloadRestart": "Downloaden en opnieuw starten",
"Updater.Status.Ready": "Huidige build: {0}",
"Updater.Status.Checking": "Controleren op updates…",
"Updater.Status.Current": "Je bent up-to-date ({0}).",
"Updater.Status.Available": "Er is een nieuwe build beschikbaar: {0}",
"Updater.Status.Downloading": "Update downloaden… {0}%",
"Updater.Status.Installing": "Update installeren…",
"Updater.Status.Timeout": "De updatecontrole is na 10 seconden verlopen.",
"Updater.Status.Failed": "Kon niet controleren op updates.",
"Updater.Status.ChecksumFailed": "De gedownloade update is niet door de SHA-256-verificatie gekomen.",
"Updater.Status.Unsupported": "Automatisch updaten vereist een x64-build voor Windows, Linux of macOS."
} }
+1 -28
View File
@@ -142,32 +142,5 @@
"About.Discord.Label": "Discord", "About.Discord.Label": "Discord",
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.", "About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
"About.GithubButton": "Contribua no GitHub!", "About.GithubButton": "Contribua no GitHub!",
"About.DiscordButton": "Junte-se ao nosso Discord!", "About.DiscordButton": "Junte-se ao nosso Discord!"
"Library.Context.GameSettings": "Definições do jogo…",
"Options.Env.WritableApp0.Desc": "Permitir que os jogos criem e escrevam ficheiros dentro da sua pasta de instalação.\nNecessário para dumps não empacotados que escrevem os seus dados guardados ou configurações em /app0.",
"Options.Env.LogIo.Desc": "Registar na consola a abertura e leitura de ficheiros e a resolução de caminhos.\nUtilize quando um jogo não encontrar os seus ficheiros de dados durante o arranque.",
"Common.Save": "Guardar",
"Common.Cancel": "Cancelar",
"PerGame.Title": "Definições por jogo — {0} ({1})",
"PerGame.InheritNote": "As linhas não assinaladas herdam as predefinições globais.",
"PerGame.EnvToggles.Label": "Variáveis de ambiente",
"PerGame.EnvToggles.Desc": "Substituir o conjunto global de opções SHARPEMU_* para este jogo.",
"About.Github.LatestCommitLabel": "Último commit",
"About.Github.LatestCommitDescription": "Último commit no ramo main",
"Updater.Auto.Label": "Procurar atualizações no arranque",
"Updater.Auto.Desc": "Consulta o GitHub sem atrasar o arranque.",
"Updater.Label": "Atualizações",
"Updater.Check": "Procurar atualizações",
"Updater.DownloadRestart": "Transferir e reiniciar",
"Updater.Status.Ready": "Build atual: {0}",
"Updater.Status.Checking": "A procurar atualizações…",
"Updater.Status.Current": "Está atualizado ({0}).",
"Updater.Status.Available": "Está disponível um novo build: {0}",
"Updater.Status.Downloading": "A transferir a atualização… {0}%",
"Updater.Status.Installing": "A instalar a atualização…",
"Updater.Status.Timeout": "A verificação de atualizações expirou após 10 segundos.",
"Updater.Status.Failed": "Não foi possível procurar atualizações.",
"Updater.Status.ChecksumFailed": "A atualização transferida falhou a verificação SHA-256.",
"Updater.Status.Unsupported": "A atualização automática requer um build x64 para Windows, Linux ou macOS."
} }
+1 -47
View File
@@ -15,7 +15,6 @@
"Library.Context.OpenFolder": "Открыть папку с игрой", "Library.Context.OpenFolder": "Открыть папку с игрой",
"Library.Context.CopyPath": "Скопировать путь", "Library.Context.CopyPath": "Скопировать путь",
"Library.Context.CopyTitleId": "Скопировать ID игры", "Library.Context.CopyTitleId": "Скопировать ID игры",
"Library.Context.GameSettings": "Настройки игры…",
"Library.Context.Remove": "Удалить из библиотеки", "Library.Context.Remove": "Удалить из библиотеки",
"Library.Empty.Title": "Ваша библиотека пуста", "Library.Empty.Title": "Ваша библиотека пуста",
@@ -27,17 +26,6 @@
"Library.Loading": "Загрузка библиотеки…", "Library.Loading": "Загрузка библиотеки…",
"Options.General": "Основные", "Options.General": "Основные",
"Options.Env.Tab": "Окружение",
"Options.Section.Environment": "ПЕРЕМЕННЫЕ ОКРУЖЕНИЯ",
"Options.Env.Desc": "Параметры, передаваемые эмулятору как переменные окружения при запуске.",
"Options.Env.Bthid.Desc": "Сообщать об отсутствии Bluetooth HID для игр, чьи библиотеки руля и обратной связи опрашивают устройство бесконечно.\nОбычно оставляйте выключенным. Некоторые игры зависают при сбое инициализации.",
"Options.Env.LoopGuard.Desc": "Не завершать принудительно игры, которые слишком долго повторяют один и тот же вызов.\nПопробуйте этот параметр, если игра сама закрывается во время загрузки.",
"Options.Env.WritableApp0.Desc": "Разрешить играм создавать и записывать файлы в папке установки.\nТребуется для неупакованных дампов, которые сохраняют данные или настройки в /app0.",
"Options.Env.VkValidation.Desc": "Включить слои валидации Vulkan для отладки GPU.\nЗамедляет работу. Требуется установленный Vulkan SDK.",
"Options.Env.DumpSpirv.Desc": "Сохранять шейдеры AGC и их переводы в SPIR-V в папку shader-dumps.\nИспользуйте при сообщении об ошибках шейдеров или рендеринга.",
"Options.Env.LogDirectMemory.Desc": "Выводить в консоль выделения прямой памяти и ошибки выделения.\nИспользуйте, если игра аварийно завершает работу или закрывается при запуске.",
"Options.Env.LogIo.Desc": "Выводить в консоль операции открытия и чтения файлов, а также разрешение путей.\nИспользуйте, если игра не может найти файлы данных при запуске.",
"Options.Env.LogNp.Desc": "Выводить в консоль вызовы библиотеки NP (PlayStation Network).",
"Options.Section.Emulation": "ЭМУЛЯЦИЯ", "Options.Section.Emulation": "ЭМУЛЯЦИЯ",
"Options.Section.Logging": "ЛОГГИРОВАНИЕ", "Options.Section.Logging": "ЛОГГИРОВАНИЕ",
"Options.Section.Launcher": "ЛАУНЧЕР", "Options.Section.Launcher": "ЛАУНЧЕР",
@@ -82,13 +70,6 @@
"Common.On": "Включено", "Common.On": "Включено",
"Common.Off": "Выключено", "Common.Off": "Выключено",
"Common.Save": "Сохранить",
"Common.Cancel": "Отмена",
"PerGame.Title": "Настройки игры — {0} ({1})",
"PerGame.InheritNote": "Неотмеченные строки наследуют глобальные настройки.",
"PerGame.EnvToggles.Label": "Переключатели окружения",
"PerGame.EnvToggles.Desc": "Переопределить глобальный набор переключателей SHARPEMU_* для этой игры.",
"Console.Title": "КОНСОЛЬ", "Console.Title": "КОНСОЛЬ",
"Console.SearchWatermark": "Поиск...", "Console.SearchWatermark": "Поиск...",
@@ -144,32 +125,5 @@
"Dialog.PsExecutables": "Исполняемые файлы PS", "Dialog.PsExecutables": "Исполняемые файлы PS",
"Dialog.SaveLogFile": "Выберите, куда сохранить файл с логами", "Dialog.SaveLogFile": "Выберите, куда сохранить файл с логами",
"Dialog.PlainTextFiles": "Текстовые файлы", "Dialog.PlainTextFiles": "Текстовые файлы",
"Dialog.LogFiles": "Логи", "Dialog.LogFiles": "Логи"
"Options.About": "О программе",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Исходный код, отчёты об ошибках и разработка проекта.",
"About.Github.LatestCommitLabel": "Последний коммит",
"About.Github.LatestCommitDescription": "Последний коммит в основной ветке",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Присоединяйтесь к сообществу, получайте поддержку и следите за разработкой.",
"About.GithubButton": "Участвовать в разработке на GitHub!",
"About.DiscordButton": "Присоединиться к нашему Discord!",
"Updater.Auto.Label": "Проверять обновления при запуске",
"Updater.Auto.Desc": "Проверяет GitHub без задержки запуска.",
"Updater.Label": "Обновления",
"Updater.Check": "Проверить обновления",
"Updater.DownloadRestart": "Скачать и перезапустить",
"Updater.Status.Ready": "Текущая сборка: {0}",
"Updater.Status.Checking": "Проверка обновлений…",
"Updater.Status.Current": "Установлена актуальная версия ({0}).",
"Updater.Status.Available": "Доступна новая сборка: {0}",
"Updater.Status.Downloading": "Скачивание обновления… {0}%",
"Updater.Status.Installing": "Установка обновления…",
"Updater.Status.Timeout": "Проверка обновлений превысила лимит времени в 10 секунд.",
"Updater.Status.Failed": "Не удалось проверить наличие обновлений.",
"Updater.Status.Unsupported": "Автоматическое обновление требует сборку Windows, Linux или macOS x64.",
"Updater.Status.ChecksumFailed": "Скачанное обновление не прошло проверку SHA-256."
} }
+1 -30
View File
@@ -140,34 +140,5 @@
"Updater.Status.Installing": "Güncelleme kuruluyor…", "Updater.Status.Installing": "Güncelleme kuruluyor…",
"Updater.Status.Timeout": "Güncelleme denetimi 10 saniye sonra zaman aşımına uğradı.", "Updater.Status.Timeout": "Güncelleme denetimi 10 saniye sonra zaman aşımına uğradı.",
"Updater.Status.Failed": "Güncellemeler denetlenemedi.", "Updater.Status.Failed": "Güncellemeler denetlenemedi.",
"Updater.Status.ChecksumFailed": "İndirilen güncelleme SHA-256 doğrulamasını geçemedi.", "Updater.Status.Unsupported": "Otomatik güncelleme Windows, Linux veya macOS x64 build'i gerektirir."
"Updater.Status.Unsupported": "Otomatik güncelleme Windows, Linux veya macOS x64 build'i gerektirir.",
"Library.Context.GameSettings": "Oyun ayarları…",
"Options.Env.Tab": "Ortam",
"Options.Section.Environment": "ORTAM DEĞİŞKENLERİ",
"Options.Env.Desc": "Başlatma sırasında emülatöre ortam değişkeni olarak geçirilen anahtarlar.",
"Options.Env.Bthid.Desc": "Direksiyon/FFB katmanı sonsuza kadar bekleyen oyunlar için Bluetooth HID'i kullanılamıyor olarak bildir.\nNormalde kapalı bırakın. Bazı oyunlar başlatma başarısız olduğunda donar.",
"Options.Env.LoopGuard.Desc": "Aynı çağrıyı uzun süre tekrarlayan oyunları zorla kapatma.\nBir oyun yükleme sırasında kendiliğinden kapanıyorsa bunu deneyin.",
"Options.Env.WritableApp0.Desc": "Oyunların kurulum klasörlerinde dosya oluşturup yazmasına izin ver.\nKayıt veya yapılandırma verisini /app0 altına yazan paketlenmemiş dump'lar için gereklidir.",
"Options.Env.VkValidation.Desc": "GPU hata ayıklaması için Vulkan doğrulama katmanlarını etkinleştir.\nYavaştır. Vulkan SDK'nın kurulu olması gerekir.",
"Options.Env.DumpSpirv.Desc": "AGC shader'larını ve SPIR-V çevirilerini shader-dumps klasörüne kaydet.\nShader veya görüntü hatalarını bildirirken kullanın.",
"Options.Env.LogDirectMemory.Desc": "Doğrudan bellek tahsislerini ve hatalarını konsola günlükle.\nBir oyun açılış sırasında çöküyor veya kapanıyorsa kullanın.",
"Options.Env.LogIo.Desc": "Dosya açma, okuma ve yol çözümleme etkinliğini konsola günlükle.\nBir oyun açılışta veri dosyalarını bulamıyorsa kullanın.",
"Options.Env.LogNp.Desc": "NP (PlayStation Network) kütüphane çağrılarını konsola günlükle.",
"Common.Save": "Kaydet",
"Common.Cancel": "İptal",
"PerGame.Title": "Oyuna özel ayarlar — {0} ({1})",
"PerGame.InheritNote": "İşaretlenmemiş satırlar genel varsayılanları kullanır.",
"PerGame.EnvToggles.Label": "Ortam anahtarları",
"PerGame.EnvToggles.Desc": "Bu oyun için genel SHARPEMU_* anahtar kümesini geçersiz kıl.",
"Options.About": "Hakkında",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Kaynak kodu, hata kayıtları ve proje geliştirme.",
"About.Github.LatestCommitLabel": "Son Commit",
"About.Github.LatestCommitDescription": "main dalındaki son commit",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Topluluğa katılın, destek alın ve geliştirmeyi takip edin.",
"About.GithubButton": "GitHub'da katkıda bulun!",
"About.DiscordButton": "Discord'umuza katıl!"
} }
+164 -88
View File
@@ -91,7 +91,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
</StackPanel> </StackPanel>
</Grid> </Grid>
<Panel Grid.Row="1" x:Name="PagesHost"> <Panel Grid.Row="1">
<!-- Library page. The tile row gets extra top margin so it sits <!-- Library page. The tile row gets extra top margin so it sits
closer to eye level (PS5 home-screen style) instead of hugging closer to eye level (PS5 home-screen style) instead of hugging
@@ -126,13 +126,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
</MenuItem.Icon> </MenuItem.Icon>
</MenuItem> </MenuItem>
<Separator /> <Separator />
<MenuItem x:Name="CtxGameSettings" Header="Game settings…">
<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" <MenuItem x:Name="CtxRemove" Header="Remove from library"
Foreground="{StaticResource DangerHoverBrush}"> Foreground="{StaticResource DangerHoverBrush}">
<MenuItem.Icon> <MenuItem.Icon>
@@ -206,19 +199,27 @@ SPDX-License-Identifier: GPL-2.0-or-later
<StackPanel Spacing="14"> <StackPanel Spacing="14">
<TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle" Text="EMULATION" /> <TextBlock x:Name="EmulationSectionTitle" Classes="sectionTitle" Text="EMULATION" />
<local:SettingRow x:Name="CpuEngineRow" Label="CPU engine" <Grid ColumnDefinitions="*,Auto">
Description="Execution engine used to run game code."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ComboBox x:Name="CpuEngineBox" Width="160" SelectedIndex="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"> VerticalAlignment="Center" CornerRadius="8">
<ComboBoxItem x:Name="CpuEngineNativeItem" Content="Native" /> <ComboBoxItem x:Name="CpuEngineNativeItem" Content="Native" />
</ComboBox> </ComboBox>
</local:SettingRow> </Grid>
<local:SettingRow x:Name="StrictRow" Label="Strict dynlib resolution" <Grid ColumnDefinitions="*,Auto">
Description="Fail the launch when an imported symbol cannot be resolved."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="StrictToggle" OnContent="On" OffContent="Off" <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" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
</StackPanel> </StackPanel>
</Border> </Border>
@@ -226,9 +227,13 @@ SPDX-License-Identifier: GPL-2.0-or-later
<StackPanel Spacing="14"> <StackPanel Spacing="14">
<TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle" Text="LOGGING" /> <TextBlock x:Name="LoggingSectionTitle" Classes="sectionTitle" Text="LOGGING" />
<local:SettingRow x:Name="LogLevelRow" Label="Log level" <Grid ColumnDefinitions="*,Auto">
Description="Verbosity of the emulator console output."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ComboBox x:Name="LogLevelBox" Width="160" SelectedIndex="2" <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"> VerticalAlignment="Center" CornerRadius="8">
<ComboBoxItem x:Name="LogLevelTraceItem" Content="Trace" /> <ComboBoxItem x:Name="LogLevelTraceItem" Content="Trace" />
<ComboBoxItem x:Name="LogLevelDebugItem" Content="Debug" /> <ComboBoxItem x:Name="LogLevelDebugItem" Content="Debug" />
@@ -237,32 +242,49 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ComboBoxItem x:Name="LogLevelErrorItem" Content="Error" /> <ComboBoxItem x:Name="LogLevelErrorItem" Content="Error" />
<ComboBoxItem x:Name="LogLevelCriticalItem" Content="Critical" /> <ComboBoxItem x:Name="LogLevelCriticalItem" Content="Critical" />
</ComboBox> </ComboBox>
</local:SettingRow> </Grid>
<local:SettingRow x:Name="TraceImportsRow" Label="Import trace limit" <Grid ColumnDefinitions="*,Auto">
Description="Trace the first N imports per module (0 = off)."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<NumericUpDown x:Name="TraceImportsBox" Width="160" Minimum="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" Maximum="4096" Increment="16" Value="0" FormatString="0"
VerticalAlignment="Center" CornerRadius="8" /> VerticalAlignment="Center" CornerRadius="8" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="LogToFileRow" Label="Log to file" <Grid ColumnDefinitions="*,Auto">
Description="Mirror emulator output to a log file."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="LogToFileToggle" OnContent="On" OffContent="Off" <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" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="LogFilePathRow" Label="Log file path" <Grid ColumnDefinitions="*,Auto">
Description="No custom path"> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<Button x:Name="SelectLogFilePathButton" Classes="ghost" Content="Select…" <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" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="OverrideLogFileRow" Label="Override log file" <Grid ColumnDefinitions="*,Auto">
Description="Use the exact file path instead of appending title ID and timestamp."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="OverrideLogFileToggle" OnContent="On" OffContent="Off" <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" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
</StackPanel> </StackPanel>
</Border> </Border>
@@ -270,30 +292,47 @@ SPDX-License-Identifier: GPL-2.0-or-later
<StackPanel Spacing="14"> <StackPanel Spacing="14">
<TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle" Text="LAUNCHER" /> <TextBlock x:Name="LauncherSectionTitle" Classes="sectionTitle" Text="LAUNCHER" />
<local:SettingRow x:Name="LanguageRow" Label="Emulator language" <Grid ColumnDefinitions="*,Auto">
Description="Language used throughout the launcher. Applies immediately."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ComboBox x:Name="LanguageBox" Width="160" <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" VerticalAlignment="Center" CornerRadius="8"
DisplayMemberBinding="{Binding NativeName}" /> DisplayMemberBinding="{Binding NativeName}" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="TitleMusicRow" Label="Title music" <Grid ColumnDefinitions="*,Auto">
Description="Loop the selected game's preview music in the library."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="TitleMusicToggle" OnContent="On" OffContent="Off" <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" /> IsChecked="True" VerticalAlignment="Center" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="DiscordRow" Label="Discord presence" <Grid ColumnDefinitions="*,Auto">
Description="Show the running game on your Discord profile."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="DiscordToggle" OnContent="On" OffContent="Off" <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" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="AutoUpdateRow" Label="Check for updates on startup" <Grid ColumnDefinitions="*,Auto">
Description="Checks GitHub without delaying startup."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="AutoUpdateToggle" OnContent="On" OffContent="Off" <TextBlock x:Name="AutoUpdateLabel" Text="Check for updates on startup" FontSize="13" />
<TextBlock x:Name="AutoUpdateDesc" Text="Checks GitHub without delaying startup."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="AutoUpdateToggle" OnContent="On" OffContent="Off"
IsChecked="True" VerticalAlignment="Center" /> IsChecked="True" VerticalAlignment="Center" />
</local:SettingRow> </Grid>
</StackPanel> </StackPanel>
</Border> </Border>
<Border Classes="card"> <Border Classes="card">
@@ -411,53 +450,93 @@ SPDX-License-Identifier: GPL-2.0-or-later
Text="Switches passed to the emulator as environment variables at launch." Text="Switches passed to the emulator as environment variables at launch."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" /> FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
<local:SettingRow x:Name="EnvBthidRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_BTHID_UNAVAILABLE" <Grid ColumnDefinitions="*,Auto">
Description="Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.&#10;Leave off normally. Some titles freeze when init fails."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="EnvBthidToggle" OnContent="On" OffContent="Off" <TextBlock Text="SHARPEMU_BTHID_UNAVAILABLE" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvBthidDesc"
Text="Report Bluetooth HID as unavailable for titles whose wheel/FFB middleware polls forever.&#10;Leave off normally. Some titles freeze when init fails."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvBthidToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="EnvLoopGuardRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD" <Grid ColumnDefinitions="*,Auto">
Description="Do not force quit titles that repeat the same call for too long.&#10;Try this when a game exits on its own while loading."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="EnvLoopGuardToggle" OnContent="On" OffContent="Off" <TextBlock Text="SHARPEMU_DISABLE_IMPORT_LOOP_GUARD" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLoopGuardDesc"
Text="Do not force quit titles that repeat the same call for too long.&#10;Try this when a game exits on its own while loading."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLoopGuardToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="EnvWritableApp0Row" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_WRITABLE_APP0" <Grid ColumnDefinitions="*,Auto">
Description="Allow titles to create and write files inside their install folder.&#10;Needed by unpackaged dumps that write their save or config data under /app0."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="EnvWritableApp0Toggle" OnContent="On" OffContent="Off" <TextBlock Text="SHARPEMU_WRITABLE_APP0" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvWritableApp0Desc"
Text="Allow titles to create and write files inside their install folder.&#10;Needed by unpackaged dumps that write their save or config data under /app0."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvWritableApp0Toggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="EnvVkValidationRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_VK_VALIDATION" <Grid ColumnDefinitions="*,Auto">
Description="Enable Vulkan validation layers for GPU debugging.&#10;Slow. Requires the Vulkan SDK to be installed."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="EnvVkValidationToggle" OnContent="On" OffContent="Off" <TextBlock Text="SHARPEMU_VK_VALIDATION" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvVkValidationDesc"
Text="Enable Vulkan validation layers for GPU debugging.&#10;Slow. Requires the Vulkan SDK to be installed."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvVkValidationToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="EnvDumpSpirvRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_DUMP_SPIRV" <Grid ColumnDefinitions="*,Auto">
Description="Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.&#10;Use when reporting shader or rendering bugs."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="EnvDumpSpirvToggle" OnContent="On" OffContent="Off" <TextBlock Text="SHARPEMU_DUMP_SPIRV" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvDumpSpirvDesc"
Text="Dump AGC shaders and their SPIR-V translations to the shader-dumps folder.&#10;Use when reporting shader or rendering bugs."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvDumpSpirvToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="EnvLogDirectMemoryRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_LOG_DIRECT_MEMORY" <Grid ColumnDefinitions="*,Auto">
Description="Log direct memory allocations and failures to the console.&#10;Use when a game aborts or exits during boot."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="EnvLogDirectMemoryToggle" OnContent="On" OffContent="Off" <TextBlock Text="SHARPEMU_LOG_DIRECT_MEMORY" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLogDirectMemoryDesc"
Text="Log direct memory allocations and failures to the console.&#10;Use when a game aborts or exits during boot."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLogDirectMemoryToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="EnvLogIoRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_LOG_IO" <Grid ColumnDefinitions="*,Auto">
Description="Log file open, read, and path-resolve activity to the console.&#10;Use when a game cannot find its data files during boot."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="EnvLogIoToggle" OnContent="On" OffContent="Off" <TextBlock Text="SHARPEMU_LOG_IO" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLogIoDesc"
Text="Log file open, read, and path-resolve activity to the console.&#10;Use when a game cannot find its data files during boot."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLogIoToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
<local:SettingRow x:Name="EnvLogNpRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_LOG_NP" <Grid ColumnDefinitions="*,Auto">
Description="Log NP (PlayStation Network) library calls to the console."> <StackPanel VerticalAlignment="Center" Spacing="2" Margin="0,0,16,0">
<ToggleSwitch x:Name="EnvLogNpToggle" OnContent="On" OffContent="Off" <TextBlock Text="SHARPEMU_LOG_NP" FontSize="13" FontFamily="Consolas,monospace" />
<TextBlock x:Name="EnvLogNpDesc"
Text="Log NP (PlayStation Network) library calls to the console."
FontSize="11" Foreground="{StaticResource MutedBrush}" TextWrapping="Wrap" />
</StackPanel>
<ToggleSwitch Grid.Column="1" x:Name="EnvLogNpToggle" OnContent="On" OffContent="Off"
VerticalAlignment="Center" /> VerticalAlignment="Center" />
</local:SettingRow> </Grid>
</StackPanel> </StackPanel>
</Border> </Border>
@@ -604,12 +683,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
<!-- This is a native popup rather than an Avalonia overlay because the <!-- This is a native popup rather than an Avalonia overlay because the
emulated Vulkan surface is a native child window. --> emulated Vulkan surface is a native child window. -->
<!-- Anchored to MainContent, not GameView: the surface host is parked in
a 1x1 corner while loading/closing, which would pull a GameView-
anchored popup into the corner with it. -->
<primitives:Popup x:Name="SessionLoadingPopup" <primitives:Popup x:Name="SessionLoadingPopup"
IsOpen="False" IsOpen="False"
PlacementTarget="{Binding #MainContent}" PlacementTarget="{Binding #GameView}"
Placement="Center" Placement="Center"
Topmost="True" Topmost="True"
ShouldUseOverlayLayer="False" ShouldUseOverlayLayer="False"
+61 -180
View File
@@ -88,15 +88,6 @@ public partial class MainWindow : Window
private int _detailLoadGeneration; private int _detailLoadGeneration;
private int _backdropGeneration; private int _backdropGeneration;
// Bundled key art shown whenever no game-specific backdrop applies; the
// plain window color remains the fallback when the asset fails to load.
private Bitmap? _defaultBackdrop;
// Whether the native loading/closing popup should be showing; it is a
// desktop-topmost popup, so it closes while the launcher is in the
// background or minimized and reopens from this flag on activation.
private bool _sessionLoadingActive;
// Controller navigation state. // Controller navigation state.
private readonly DispatcherTimer _gamepadTimer; private readonly DispatcherTimer _gamepadTimer;
private HostGamepadButtons _previousPadButtons; private HostGamepadButtons _previousPadButtons;
@@ -113,25 +104,12 @@ public partial class MainWindow : Window
string EbootPath, string EbootPath,
string DisplayName, string DisplayName,
string? TitleId, string? TitleId,
string LogLevel,
SharpEmuRuntimeOptions RuntimeOptions); SharpEmuRuntimeOptions RuntimeOptions);
public MainWindow() public MainWindow()
{ {
InitializeComponent(); InitializeComponent();
try
{
_defaultBackdrop = new Bitmap(
AssetLoader.Open(new Uri("avares://SharpEmu.GUI/Assets/pic0.png")));
BackdropImage.Source = _defaultBackdrop;
BackdropImage.Opacity = 1.0;
}
catch (Exception)
{
_defaultBackdrop = null; // color background remains the fallback
}
GameList.ItemsSource = _visibleGames; GameList.ItemsSource = _visibleGames;
ConsoleList.ItemsSource = _consoleLines; ConsoleList.ItemsSource = _consoleLines;
_consoleMirror = GuiConsoleMirror.Install((line, isError) => _consoleMirror = GuiConsoleMirror.Install((line, isError) =>
@@ -155,18 +133,8 @@ public partial class MainWindow : Window
}; };
_libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur(); _libraryBlurTimer.Tick += (_, _) => AdvanceLibraryBlur();
// Native popups float above every window on the desktop; they must Activated += (_, _) => UpdateSessionBarVisibility();
// follow the launcher into the background or a minimized state. Deactivated += (_, _) => SessionBarPopup.IsOpen = false;
Activated += (_, _) =>
{
UpdateSessionBarVisibility();
SessionLoadingPopup.IsOpen = _sessionLoadingActive;
};
Deactivated += (_, _) =>
{
SessionBarPopup.IsOpen = false;
SessionLoadingPopup.IsOpen = false;
};
TitleBar.PointerPressed += OnTitleBarPointerPressed; TitleBar.PointerPressed += OnTitleBarPointerPressed;
GameList.SelectionChanged += (_, _) => UpdateSelectedGame(); GameList.SelectionChanged += (_, _) => UpdateSelectedGame();
@@ -236,7 +204,6 @@ public partial class MainWindow : Window
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path, "Clipboard.Path"); await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.Path, "Clipboard.Path");
CtxCopyTitleId.Click += async (_, _) => CtxCopyTitleId.Click += async (_, _) =>
await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId, "Clipboard.TitleId"); await CopyToClipboardAsync((GameList.SelectedItem as GameEntry)?.TitleId, "Clipboard.TitleId");
CtxGameSettings.Click += (_, _) => OpenSelectedGameSettings();
CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary(); CtxRemove.Click += (_, _) => RemoveSelectedFromLibrary();
Opened += async (_, _) => await OnOpenedAsync(); Opened += async (_, _) => await OnOpenedAsync();
@@ -429,15 +396,6 @@ public partial class MainWindow : Window
return; return;
} }
if (_isRunning || _isStopping)
{
// The game renders inside the launcher window, so the launcher
// stays active while playing. The controller belongs to the game
// then: no navigation, and Circle/B must never stop the session.
_previousPadButtons = pad.Buttons;
return;
}
var shoulderPressed = pad.Buttons & ~_previousPadButtons; var shoulderPressed = pad.Buttons & ~_previousPadButtons;
if ((shoulderPressed & HostGamepadButtons.L1) != 0) if ((shoulderPressed & HostGamepadButtons.L1) != 0)
{ {
@@ -487,6 +445,11 @@ public partial class MainWindow : Window
LaunchSelected(); LaunchSelected();
} }
if ((pressed & HostGamepadButtons.Circle) != 0)
{
StopEmulator();
}
_previousPadButtons = pad.Buttons; _previousPadButtons = pad.Buttons;
} }
@@ -610,7 +573,6 @@ public partial class MainWindow : Window
CtxOpenFolder.Header = loc.Get("Library.Context.OpenFolder"); CtxOpenFolder.Header = loc.Get("Library.Context.OpenFolder");
CtxCopyPath.Header = loc.Get("Library.Context.CopyPath"); CtxCopyPath.Header = loc.Get("Library.Context.CopyPath");
CtxCopyTitleId.Header = loc.Get("Library.Context.CopyTitleId"); CtxCopyTitleId.Header = loc.Get("Library.Context.CopyTitleId");
CtxGameSettings.Header = loc.Get("Library.Context.GameSettings");
CtxRemove.Header = loc.Get("Library.Context.Remove"); CtxRemove.Header = loc.Get("Library.Context.Remove");
EmptyAddFolderButton.Content = loc.Get("Library.Empty.AddFolder"); EmptyAddFolderButton.Content = loc.Get("Library.Empty.AddFolder");
@@ -620,27 +582,27 @@ public partial class MainWindow : Window
EnvTabItem.Header = loc.Get("Options.Env.Tab"); EnvTabItem.Header = loc.Get("Options.Env.Tab");
EnvSectionTitle.Text = loc.Get("Options.Section.Environment"); EnvSectionTitle.Text = loc.Get("Options.Section.Environment");
EnvDesc.Text = loc.Get("Options.Env.Desc"); EnvDesc.Text = loc.Get("Options.Env.Desc");
EnvBthidRow.Description = loc.Get("Options.Env.Bthid.Desc"); EnvBthidDesc.Text = loc.Get("Options.Env.Bthid.Desc");
EnvLoopGuardRow.Description = loc.Get("Options.Env.LoopGuard.Desc"); EnvLoopGuardDesc.Text = loc.Get("Options.Env.LoopGuard.Desc");
EnvWritableApp0Row.Description = loc.Get("Options.Env.WritableApp0.Desc"); EnvWritableApp0Desc.Text = loc.Get("Options.Env.WritableApp0.Desc");
EnvVkValidationRow.Description = loc.Get("Options.Env.VkValidation.Desc"); EnvVkValidationDesc.Text = loc.Get("Options.Env.VkValidation.Desc");
EnvDumpSpirvRow.Description = loc.Get("Options.Env.DumpSpirv.Desc"); EnvDumpSpirvDesc.Text = loc.Get("Options.Env.DumpSpirv.Desc");
EnvLogDirectMemoryRow.Description = loc.Get("Options.Env.LogDirectMemory.Desc"); EnvLogDirectMemoryDesc.Text = loc.Get("Options.Env.LogDirectMemory.Desc");
EnvLogIoRow.Description = loc.Get("Options.Env.LogIo.Desc"); EnvLogIoDesc.Text = loc.Get("Options.Env.LogIo.Desc");
EnvLogNpRow.Description = loc.Get("Options.Env.LogNp.Desc"); EnvLogNpDesc.Text = loc.Get("Options.Env.LogNp.Desc");
EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation"); EmulationSectionTitle.Text = loc.Get("Options.Section.Emulation");
LoggingSectionTitle.Text = loc.Get("Options.Section.Logging"); LoggingSectionTitle.Text = loc.Get("Options.Section.Logging");
LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher"); LauncherSectionTitle.Text = loc.Get("Options.Section.Launcher");
CpuEngineRow.Label = loc.Get("Options.CpuEngine.Label"); CpuEngineLabel.Text = loc.Get("Options.CpuEngine.Label");
CpuEngineRow.Description = loc.Get("Options.CpuEngine.Desc"); CpuEngineDesc.Text = loc.Get("Options.CpuEngine.Desc");
CpuEngineNativeItem.Content = loc.Get("Options.CpuEngine.Native"); CpuEngineNativeItem.Content = loc.Get("Options.CpuEngine.Native");
StrictRow.Label = loc.Get("Options.Strict.Label"); StrictLabel.Text = loc.Get("Options.Strict.Label");
StrictRow.Description = loc.Get("Options.Strict.Desc"); StrictDesc.Text = loc.Get("Options.Strict.Desc");
LogLevelRow.Label = loc.Get("Options.LogLevel.Label"); LogLevelLabel.Text = loc.Get("Options.LogLevel.Label");
LogLevelRow.Description = loc.Get("Options.LogLevel.Desc"); LogLevelDesc.Text = loc.Get("Options.LogLevel.Desc");
LogLevelTraceItem.Content = loc.Get("Options.LogLevel.Trace"); LogLevelTraceItem.Content = loc.Get("Options.LogLevel.Trace");
LogLevelDebugItem.Content = loc.Get("Options.LogLevel.Debug"); LogLevelDebugItem.Content = loc.Get("Options.LogLevel.Debug");
LogLevelInfoItem.Content = loc.Get("Options.LogLevel.Info"); LogLevelInfoItem.Content = loc.Get("Options.LogLevel.Info");
@@ -648,29 +610,29 @@ public partial class MainWindow : Window
LogLevelErrorItem.Content = loc.Get("Options.LogLevel.Error"); LogLevelErrorItem.Content = loc.Get("Options.LogLevel.Error");
LogLevelCriticalItem.Content = loc.Get("Options.LogLevel.Critical"); LogLevelCriticalItem.Content = loc.Get("Options.LogLevel.Critical");
TraceImportsRow.Label = loc.Get("Options.TraceImports.Label"); TraceImportsLabel.Text = loc.Get("Options.TraceImports.Label");
TraceImportsRow.Description = loc.Get("Options.TraceImports.Desc"); TraceImportsDesc.Text = loc.Get("Options.TraceImports.Desc");
LogToFileRow.Label = loc.Get("Options.LogToFile.Label"); LogToFileLabel.Text = loc.Get("Options.LogToFile.Label");
LogToFileRow.Description = loc.Get("Options.LogToFile.Desc"); LogToFileDesc.Text = loc.Get("Options.LogToFile.Desc");
LogFilePathRow.Label = loc.Get("Options.LogFilePath.Label"); LogFilePathLabel.Text = loc.Get("Options.LogFilePath.Label");
SelectLogFilePathButton.Content = loc.Get("Options.LogFilePath.Select"); SelectLogFilePathButton.Content = loc.Get("Options.LogFilePath.Select");
UpdateLogFilePathText(); UpdateLogFilePathText();
OverrideLogFileRow.Label = loc.Get("Options.OverrideLogFile.Label"); OverrideLogFileLabel.Text = loc.Get("Options.OverrideLogFile.Label");
OverrideLogFileRow.Description = loc.Get("Options.OverrideLogFile.Desc"); OverrideLogFileDesc.Text = loc.Get("Options.OverrideLogFile.Desc");
LanguageRow.Label = loc.Get("Options.Language.Label"); LanguageLabel.Text = loc.Get("Options.Language.Label");
LanguageRow.Description = loc.Get("Options.Language.Desc"); LanguageDesc.Text = loc.Get("Options.Language.Desc");
TitleMusicRow.Label = loc.Get("Options.TitleMusic.Label"); TitleMusicLabel.Text = loc.Get("Options.TitleMusic.Label");
TitleMusicRow.Description = loc.Get("Options.TitleMusic.Desc"); TitleMusicDesc.Text = loc.Get("Options.TitleMusic.Desc");
DiscordRow.Label = loc.Get("Options.Discord.Label"); DiscordLabel.Text = loc.Get("Options.Discord.Label");
DiscordRow.Description = loc.Get("Options.Discord.Desc"); DiscordDesc.Text = loc.Get("Options.Discord.Desc");
AutoUpdateRow.Label = loc.Get("Updater.Auto.Label"); AutoUpdateLabel.Text = loc.Get("Updater.Auto.Label");
AutoUpdateRow.Description = loc.Get("Updater.Auto.Desc"); AutoUpdateDesc.Text = loc.Get("Updater.Auto.Desc");
foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle, AutoUpdateToggle }) foreach (var toggle in new[] { StrictToggle, LogToFileToggle, OverrideLogFileToggle, TitleMusicToggle, DiscordToggle, AutoUpdateToggle })
{ {
@@ -903,11 +865,6 @@ public partial class MainWindow : Window
SetUpdateStatus("Updater.Status.Installing"); SetUpdateStatus("Updater.Status.Installing");
Close(); Close();
} }
catch (InvalidDataException)
{
SetUpdateStatus("Updater.Status.ChecksumFailed");
UpdateButton.IsEnabled = true;
}
catch catch
{ {
SetUpdateStatus("Updater.Status.Failed"); SetUpdateStatus("Updater.Status.Failed");
@@ -995,7 +952,7 @@ public partial class MainWindow : Window
private void UpdateLogFilePathText() private void UpdateLogFilePathText()
{ {
LogFilePathRow.Description = string.IsNullOrWhiteSpace(_settings.LogFilePath) LogFilePathText.Text = string.IsNullOrWhiteSpace(_settings.LogFilePath)
? Localization.Instance.Get("Options.LogFilePath.Default") ? Localization.Instance.Get("Options.LogFilePath.Default")
: _settings.LogFilePath; : _settings.LogFilePath;
} }
@@ -1422,25 +1379,6 @@ public partial class MainWindow : Window
GameList.SelectedItem = game; GameList.SelectedItem = game;
CtxLaunch.IsEnabled = !_isRunning; CtxLaunch.IsEnabled = !_isRunning;
CtxCopyTitleId.IsEnabled = game.TitleId is not null; CtxCopyTitleId.IsEnabled = game.TitleId is not null;
CtxGameSettings.IsEnabled = !string.IsNullOrWhiteSpace(game.TitleId);
}
private void OpenSelectedGameSettings()
{
if (GameList.SelectedItem is not GameEntry game)
{
return;
}
if (string.IsNullOrWhiteSpace(game.TitleId))
{
AppendConsoleLine(
"[GUI][WARN] Per-game settings require a title ID, which this game does not have.",
WarningLineBrush);
return;
}
_ = new PerGameSettingsDialog(game.TitleId, game.Name, _settings).ShowDialog(this);
} }
private void OpenSelectedGameFolder() private void OpenSelectedGameFolder()
@@ -1645,23 +1583,13 @@ public partial class MainWindow : Window
base.OnPropertyChanged(change); base.OnPropertyChanged(change);
if (change.Property == WindowStateProperty) if (change.Property == WindowStateProperty)
{ {
// The XAML WindowState="Maximized" assignment raises this change
// during InitializeComponent, before named controls are wired up.
if (WindowState == WindowState.Minimized) if (WindowState == WindowState.Minimized)
{ {
_sndPreview.Pause(); _sndPreview.Pause();
if (SessionLoadingPopup is { } popup)
{
popup.IsOpen = false;
}
} }
else else
{ {
_sndPreview.Resume(); _sndPreview.Resume();
if (SessionLoadingPopup is { } popup)
{
popup.IsOpen = _sessionLoadingActive;
}
} }
} }
} }
@@ -1676,20 +1604,8 @@ public partial class MainWindow : Window
var generation = ++_backdropGeneration; var generation = ++_backdropGeneration;
BackdropImage.Opacity = 0; BackdropImage.Opacity = 0;
// The bundled key art is the primary backdrop whenever the selection
// has no art of its own; the window color stays as the last fallback.
void ShowDefaultBackdrop()
{
if (generation == _backdropGeneration && _defaultBackdrop is not null)
{
BackdropImage.Source = _defaultBackdrop;
BackdropImage.Opacity = 1.0;
}
}
if (game?.BackgroundPath is null) if (game?.BackgroundPath is null)
{ {
ShowDefaultBackdrop();
return; return;
} }
@@ -1706,8 +1622,7 @@ public partial class MainWindow : Window
} }
catch (Exception) catch (Exception)
{ {
ShowDefaultBackdrop(); // undecodable key art return; // undecodable key art: keep the plain background
return;
} }
} }
@@ -1756,39 +1671,34 @@ public partial class MainWindow : Window
return; return;
} }
var resolvedTitleId = string.IsNullOrWhiteSpace(titleId)
? _allGames.FirstOrDefault(game => game.Path.Equals(ebootPath, FilePathComparison))?.TitleId
: titleId;
var effective = EffectiveLaunchSettings.Resolve(_settings, PerGameSettings.Load(resolvedTitleId));
_sndPreview.Stop(); _sndPreview.Stop();
_consoleLines.Clear(); _consoleLines.Clear();
_allConsoleLines.Clear(); _allConsoleLines.Clear();
DropFileLog(); DropFileLog();
if (effective.LogToFile) if (_settings.LogToFile)
{ {
OpenFileLog(resolvedTitleId); OpenFileLog(titleId);
} }
// The isolated game child inherits these diagnostics. Keep them on the // The isolated game child inherits these diagnostics. Keep them on the
// launcher process so every platform receives the same launch options. // launcher process so every platform receives the same launch options.
foreach (var staleName in _appliedEnvironmentVariables) foreach (var staleName in _appliedEnvironmentVariables)
{ {
if (!effective.EnvironmentToggles.Contains(staleName)) if (!_settings.EnvironmentToggles.Contains(staleName))
{ {
Environment.SetEnvironmentVariable(staleName, null); Environment.SetEnvironmentVariable(staleName, null);
} }
} }
_appliedEnvironmentVariables.Clear(); _appliedEnvironmentVariables.Clear();
foreach (var name in effective.EnvironmentToggles) foreach (var name in _settings.EnvironmentToggles)
{ {
Environment.SetEnvironmentVariable(name, "1"); Environment.SetEnvironmentVariable(name, "1");
_appliedEnvironmentVariables.Add(name); _appliedEnvironmentVariables.Add(name);
} }
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel)) if (SharpEmuLog.TryParseLevel(_settings.LogLevel, out var logLevel))
{ {
SharpEmuLog.MinimumLevel = logLevel; SharpEmuLog.MinimumLevel = logLevel;
} }
@@ -1796,14 +1706,15 @@ public partial class MainWindow : Window
var runtimeOptions = new SharpEmuRuntimeOptions var runtimeOptions = new SharpEmuRuntimeOptions
{ {
CpuEngine = CpuExecutionEngine.NativeOnly, CpuEngine = CpuExecutionEngine.NativeOnly,
StrictDynlibResolution = effective.StrictDynlibResolution, StrictDynlibResolution = _settings.StrictDynlibResolution,
ImportTraceLimit = Math.Max(0, effective.ImportTraceLimit), ImportTraceLimit = Math.Max(0, _settings.ImportTraceLimit),
}; };
_isRunning = true; _isRunning = true;
_runningGameName = displayName; _runningGameName = displayName;
SessionGameTitle.Text = displayName; SessionGameTitle.Text = displayName;
_runningGameTitleId = resolvedTitleId; _runningGameTitleId = titleId ?? _allGames
.FirstOrDefault(game => game.Path.Equals(ebootPath, FilePathComparison))?.TitleId;
_runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds(); _runningSinceUnixSeconds = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
StatusDot.Fill = SuccessLineBrush; StatusDot.Fill = SuccessLineBrush;
StatusText.Text = Localization.Instance.Format("Launch.Running", displayName); StatusText.Text = Localization.Instance.Format("Launch.Running", displayName);
@@ -1816,7 +1727,6 @@ public partial class MainWindow : Window
Path.GetFullPath(ebootPath), Path.GetFullPath(ebootPath),
displayName, displayName,
_runningGameTitleId, _runningGameTitleId,
effective.LogLevel,
runtimeOptions); runtimeOptions);
if (_gameSurfaceHost?.Surface is { } surface) if (_gameSurfaceHost?.Surface is { } surface)
@@ -1985,7 +1895,7 @@ public partial class MainWindow : Window
var arguments = new List<string> var arguments = new List<string>
{ {
"--cpu-engine=native", "--cpu-engine=native",
$"--log-level={launch.LogLevel}", $"--log-level={_settings.LogLevel}",
}; };
if (launch.RuntimeOptions.StrictDynlibResolution) if (launch.RuntimeOptions.StrictDynlibResolution)
{ {
@@ -2027,7 +1937,6 @@ public partial class MainWindow : Window
_awaitingFirstFrame = false; _awaitingFirstFrame = false;
ClearLibraryBlur(); ClearLibraryBlur();
MainContent.Margin = new Thickness(0); MainContent.Margin = new Thickness(0);
RestoreGameViewToFull();
GameView.Background = Brushes.Black; GameView.Background = Brushes.Black;
GameView.IsHitTestVisible = true; GameView.IsHitTestVisible = true;
_gameSurfaceHost?.SetPresentationVisible(true); _gameSurfaceHost?.SetPresentationVisible(true);
@@ -2038,7 +1947,7 @@ public partial class MainWindow : Window
ContentToolbar.IsVisible = false; ContentToolbar.IsVisible = false;
ConsolePanel.IsVisible = false; ConsolePanel.IsVisible = false;
LaunchBar.IsVisible = false; LaunchBar.IsVisible = false;
HideSessionLoading(); SessionLoadingPopup.IsOpen = false;
UpdateSessionBarVisibility(); UpdateSessionBarVisibility();
} }
}); });
@@ -2089,31 +1998,11 @@ public partial class MainWindow : Window
} }
} }
/// <summary>
/// The native host attachment is a real child window: it sits above every
/// Avalonia control it covers and swallows their mouse input regardless of
/// hit-test settings. While the library must stay interactive (loading,
/// closing), the surface is parked offscreen AT FULL SIZE via a negative
/// margin. It must not be shrunk instead: the emulator child polls the
/// HWND client size and its presenter defers swapchain creation while the
/// surface is 1px, which would deadlock the loading handshake.
/// </summary>
private void ParkGameViewOffscreen()
{
GameView.Margin = new Thickness(-20000, 0, 20000, 0);
}
private void RestoreGameViewToFull()
{
GameView.Margin = new Thickness(0);
}
private void ShowGameView() private void ShowGameView()
{ {
_isStopping = false; _isStopping = false;
_awaitingFirstFrame = true; _awaitingFirstFrame = true;
var host = EnsureGameSurfaceHost(); var host = EnsureGameSurfaceHost();
ParkGameViewOffscreen();
GameView.IsVisible = true; GameView.IsVisible = true;
GameView.Background = Brushes.Transparent; GameView.Background = Brushes.Transparent;
GameView.IsHitTestVisible = false; GameView.IsHitTestVisible = false;
@@ -2138,7 +2027,7 @@ public partial class MainWindow : Window
GameView.IsVisible = false; GameView.IsVisible = false;
GameView.IsHitTestVisible = true; GameView.IsHitTestVisible = true;
SessionBarPopup.IsOpen = false; SessionBarPopup.IsOpen = false;
HideSessionLoading(); SessionLoadingPopup.IsOpen = false;
AnimateLibraryBlur(0, clearWhenComplete: true); AnimateLibraryBlur(0, clearWhenComplete: true);
MainContent.Margin = new Thickness(32, 24, 32, 20); MainContent.Margin = new Thickness(32, 24, 32, 20);
ContentToolbar.IsVisible = true; ContentToolbar.IsVisible = true;
@@ -2147,15 +2036,16 @@ public partial class MainWindow : Window
LibraryPage.IsVisible = _activePageIndex == 0; LibraryPage.IsVisible = _activePageIndex == 0;
LibraryToolbar.IsVisible = _activePageIndex == 0; LibraryToolbar.IsVisible = _activePageIndex == 0;
OptionsPage.IsVisible = _activePageIndex == 1; OptionsPage.IsVisible = _activePageIndex == 1;
// Game art when the source still holds it, otherwise the bundled if (GameList.SelectedItem is GameEntry game && game.Background is not null)
// default; a bare color only when neither is available. {
BackdropImage.Opacity = BackdropImage.Source is not null ? 1 : 0; BackdropImage.Opacity = 1;
}
} }
private void AnimateLibraryBlur(double targetRadius, bool clearWhenComplete = false) private void AnimateLibraryBlur(double targetRadius, bool clearWhenComplete = false)
{ {
_libraryBlur ??= new BlurEffect(); _libraryBlur ??= new BlurEffect();
PagesHost.Effect = _libraryBlur; MainContent.Effect = _libraryBlur;
_libraryBlurStartRadius = _libraryBlur.Radius; _libraryBlurStartRadius = _libraryBlur.Radius;
_libraryBlurTargetRadius = Math.Max(0, targetRadius); _libraryBlurTargetRadius = Math.Max(0, targetRadius);
@@ -2204,7 +2094,7 @@ public partial class MainWindow : Window
if (_clearLibraryBlurWhenComplete) if (_clearLibraryBlurWhenComplete)
{ {
PagesHost.Effect = null; MainContent.Effect = null;
_libraryBlur = null; _libraryBlur = null;
_clearLibraryBlurWhenComplete = false; _clearLibraryBlurWhenComplete = false;
} }
@@ -2215,21 +2105,14 @@ public partial class MainWindow : Window
_libraryBlurTimer.Stop(); _libraryBlurTimer.Stop();
_libraryBlur = null; _libraryBlur = null;
_clearLibraryBlurWhenComplete = false; _clearLibraryBlurWhenComplete = false;
PagesHost.Effect = null; MainContent.Effect = null;
} }
private void ShowSessionLoading(string title, string detail) private void ShowSessionLoading(string title, string detail)
{ {
SessionLoadingTitle.Text = title; SessionLoadingTitle.Text = title;
SessionLoadingDetail.Text = detail; SessionLoadingDetail.Text = detail;
_sessionLoadingActive = true; SessionLoadingPopup.IsOpen = true;
SessionLoadingPopup.IsOpen = IsActive && WindowState != WindowState.Minimized;
}
private void HideSessionLoading()
{
_sessionLoadingActive = false;
SessionLoadingPopup.IsOpen = false;
} }
private void ReturnToLibraryWhileStopping() private void ReturnToLibraryWhileStopping()
@@ -2241,12 +2124,10 @@ public partial class MainWindow : Window
// Keep the native child alive until the session exits, but hide it // Keep the native child alive until the session exits, but hide it
// immediately. Destroying it while Vulkan still owns the surface can // immediately. Destroying it while Vulkan still owns the surface can
// crash the GUI; parking it in the 1x1 corner lets the library // crash the GUI; leaving it transparent lets the library recover
// recover — and stay clickable — while the native closing popup // while the native closing popup reports teardown progress.
// reports teardown progress.
_gameSurfaceHost?.SetPresentationVisible(false); _gameSurfaceHost?.SetPresentationVisible(false);
_awaitingFirstFrame = false; _awaitingFirstFrame = false;
ParkGameViewOffscreen();
GameView.Background = Brushes.Transparent; GameView.Background = Brushes.Transparent;
GameView.IsHitTestVisible = false; GameView.IsHitTestVisible = false;
SessionBarPopup.IsOpen = false; SessionBarPopup.IsOpen = false;
@@ -2258,7 +2139,7 @@ public partial class MainWindow : Window
LibraryPage.IsVisible = _activePageIndex == 0; LibraryPage.IsVisible = _activePageIndex == 0;
LibraryToolbar.IsVisible = _activePageIndex == 0; LibraryToolbar.IsVisible = _activePageIndex == 0;
OptionsPage.IsVisible = _activePageIndex == 1; OptionsPage.IsVisible = _activePageIndex == 1;
BackdropImage.Opacity = BackdropImage.Source is not null ? 1 : 0; BackdropImage.Opacity = GameList.SelectedItem is GameEntry { Background: not null } ? 1 : 0;
UpdateRunButtons(); UpdateRunButtons();
Console.Error.WriteLine("[GUI][INFO] Library restored while embedded session is closing."); Console.Error.WriteLine("[GUI][INFO] Library restored while embedded session is closing.");
} }
-115
View File
@@ -1,115 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text.Json;
using System.Text.Json.Serialization;
namespace SharpEmu.GUI;
public sealed class PerGameSettings
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
WriteIndented = true,
};
public string? LogLevel { get; set; }
public int? ImportTraceLimit { get; set; }
public bool? StrictDynlibResolution { get; set; }
public bool? LogToFile { get; set; }
public List<string>? EnvironmentToggles { get; set; }
[JsonIgnore]
public bool IsEmpty =>
LogLevel is null &&
ImportTraceLimit is null &&
StrictDynlibResolution is null &&
LogToFile is null &&
EnvironmentToggles is null;
public static string DirectoryPath =>
Path.Combine(AppContext.BaseDirectory, "user", "custom_configs");
public static string PathFor(string titleId) =>
Path.Combine(DirectoryPath, SanitizeTitleId(titleId) + ".json");
public static PerGameSettings? Load(string? titleId)
{
if (string.IsNullOrWhiteSpace(titleId))
{
return null;
}
try
{
var path = PathFor(titleId);
if (File.Exists(path))
{
return JsonSerializer.Deserialize<PerGameSettings>(File.ReadAllText(path), SerializerOptions);
}
}
catch (Exception)
{
}
return null;
}
public void Save(string titleId)
{
if (string.IsNullOrWhiteSpace(titleId))
{
return;
}
try
{
var path = PathFor(titleId);
if (IsEmpty)
{
if (File.Exists(path))
{
File.Delete(path);
}
return;
}
Directory.CreateDirectory(DirectoryPath);
File.WriteAllText(path, JsonSerializer.Serialize(this, SerializerOptions));
}
catch (Exception)
{
}
}
private static string SanitizeTitleId(string titleId)
{
var trimmed = titleId.Trim();
foreach (var invalid in Path.GetInvalidFileNameChars())
{
trimmed = trimmed.Replace(invalid, '_');
}
return trimmed.Length == 0 ? "UNKNOWN" : trimmed;
}
}
public sealed record EffectiveLaunchSettings(
string LogLevel,
int ImportTraceLimit,
bool StrictDynlibResolution,
bool LogToFile,
IReadOnlyList<string> EnvironmentToggles)
{
public static EffectiveLaunchSettings Resolve(GuiSettings global, PerGameSettings? perGame) => new(
perGame?.LogLevel ?? global.LogLevel,
perGame?.ImportTraceLimit ?? global.ImportTraceLimit,
perGame?.StrictDynlibResolution ?? global.StrictDynlibResolution,
perGame?.LogToFile ?? global.LogToFile,
perGame?.EnvironmentToggles ?? global.EnvironmentToggles);
}
-205
View File
@@ -1,205 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
using Avalonia.Controls;
using Avalonia.Layout;
using Avalonia.Media;
namespace SharpEmu.GUI;
public sealed class PerGameSettingsDialog : Window
{
private static readonly string[] LogLevels =
{ "Trace", "Debug", "Info", "Warning", "Error", "Critical" };
private static readonly string[] EnvToggles =
{
"SHARPEMU_BTHID_UNAVAILABLE",
"SHARPEMU_DISABLE_IMPORT_LOOP_GUARD",
"SHARPEMU_WRITABLE_APP0",
"SHARPEMU_VK_VALIDATION",
"SHARPEMU_DUMP_SPIRV",
"SHARPEMU_LOG_DIRECT_MEMORY",
"SHARPEMU_LOG_IO",
"SHARPEMU_LOG_NP",
};
private readonly string _titleId;
private readonly SettingRow _logLevelRow;
private readonly ComboBox _logLevel = new() { ItemsSource = LogLevels, Width = 160 };
private readonly SettingRow _traceRow;
private readonly NumericUpDown _trace = new()
{
Minimum = 0, Maximum = 4096, Increment = 16, Width = 160, FormatString = "0",
};
private readonly SettingRow _strictRow;
private readonly ToggleSwitch _strict = new();
private readonly SettingRow _logToFileRow;
private readonly ToggleSwitch _logToFile = new();
private readonly SettingRow _envRow;
private readonly StackPanel _envList = new() { Orientation = Orientation.Vertical, Spacing = 8, Margin = new(0, 4, 0, 0) };
private readonly List<(string Name, ToggleSwitch Box)> _envBoxes = new();
public PerGameSettingsDialog(string titleId, string displayName, GuiSettings global)
{
_titleId = titleId;
var loc = Localization.Instance;
Title = loc.Format("PerGame.Title", displayName, titleId);
Width = 520;
MaxHeight = 720;
SizeToContent = SizeToContent.Height;
WindowStartupLocation = WindowStartupLocation.CenterOwner;
CanResize = false;
Background = new SolidColorBrush(Color.Parse("#0D1017"));
_strict.OnContent = _logToFile.OnContent = loc.Get("Common.On");
_strict.OffContent = _logToFile.OffContent = loc.Get("Common.Off");
_logLevelRow = Row(loc.Get("Options.LogLevel.Label"), loc.Get("Options.LogLevel.Desc"), _logLevel);
_traceRow = Row(loc.Get("Options.TraceImports.Label"), loc.Get("Options.TraceImports.Desc"), _trace);
_strictRow = Row(loc.Get("Options.Strict.Label"), loc.Get("Options.Strict.Desc"), _strict);
_logToFileRow = Row(loc.Get("Options.LogToFile.Label"), loc.Get("Options.LogToFile.Desc"), _logToFile);
_envRow = new SettingRow
{
Label = loc.Get("PerGame.EnvToggles.Label"),
Description = loc.Get("PerGame.EnvToggles.Desc"),
ShowOverride = true,
};
foreach (var name in EnvToggles)
{
var box = new ToggleSwitch { OnContent = name, OffContent = name };
_envBoxes.Add((name, box));
_envList.Children.Add(box);
}
var content = new StackPanel { Orientation = Orientation.Vertical, Spacing = 12, Margin = new(16) };
content.Children.Add(new TextBlock
{
Text = loc.Get("PerGame.InheritNote"),
Foreground = new SolidColorBrush(Color.Parse("#8B94A7")),
FontSize = 12,
});
content.Children.Add(Card(loc.Get("Options.Section.Emulation"), _strictRow));
content.Children.Add(Card(loc.Get("Options.Section.Logging"), _logLevelRow, _traceRow, _logToFileRow));
content.Children.Add(Card(loc.Get("Options.Section.Environment"), _envRow, _envList));
var save = new Button { Content = loc.Get("Common.Save"), Classes = { "accent" } };
var cancel = new Button { Content = loc.Get("Common.Cancel"), Classes = { "ghost" } };
save.Click += (_, _) => { Persist(); Close(); };
cancel.Click += (_, _) => Close();
var buttonBar = new Border
{
BorderBrush = new SolidColorBrush(Color.Parse("#8B94A7")) { Opacity = 0.25 },
BorderThickness = new Thickness(0, 1, 0, 0),
Padding = new(16),
Child = new StackPanel
{
Orientation = Orientation.Horizontal,
Spacing = 8,
HorizontalAlignment = HorizontalAlignment.Right,
Children = { cancel, save },
},
};
var root = new Grid { RowDefinitions = new RowDefinitions("*,Auto") };
var scroller = new ScrollViewer { Content = content };
Grid.SetRow(scroller, 0);
Grid.SetRow(buttonBar, 1);
root.Children.Add(scroller);
root.Children.Add(buttonBar);
Content = root;
LoadValues(global);
_envRow.PropertyChanged += (_, e) =>
{
if (e.Property == SettingRow.IsOverriddenProperty)
{
_envList.IsEnabled = _envRow.IsOverridden;
}
};
_envList.IsEnabled = _envRow.IsOverridden;
}
private static SettingRow Row(string label, string description, Control value) => new()
{
Label = label,
Description = description,
ShowOverride = true,
Content = value,
};
private static Border Card(string title, params Control[] rows)
{
var stack = new StackPanel { Orientation = Orientation.Vertical, Spacing = 14 };
stack.Children.Add(new TextBlock { Text = title, Classes = { "sectionTitle" } });
foreach (var row in rows)
{
stack.Children.Add(row);
}
var card = new Border { Child = stack };
card.Classes.Add("card");
return card;
}
private void LoadValues(GuiSettings global)
{
_logLevel.SelectedItem = Array.IndexOf(LogLevels, global.LogLevel) >= 0 ? global.LogLevel : "Info";
_trace.Value = global.ImportTraceLimit;
_strict.IsChecked = global.StrictDynlibResolution;
_logToFile.IsChecked = global.LogToFile;
foreach (var (name, box) in _envBoxes)
{
box.IsChecked = global.EnvironmentToggles.Contains(name);
}
var existing = PerGameSettings.Load(_titleId);
if (existing is null)
{
return;
}
if (existing.LogLevel is { } level && Array.IndexOf(LogLevels, level) >= 0)
{
_logLevelRow.IsOverridden = true;
_logLevel.SelectedItem = level;
}
if (existing.ImportTraceLimit is { } t) { _traceRow.IsOverridden = true; _trace.Value = t; }
if (existing.StrictDynlibResolution is { } s) { _strictRow.IsOverridden = true; _strict.IsChecked = s; }
if (existing.LogToFile is { } l) { _logToFileRow.IsOverridden = true; _logToFile.IsChecked = l; }
if (existing.EnvironmentToggles is { } env)
{
_envRow.IsOverridden = true;
foreach (var (name, box) in _envBoxes)
{
box.IsChecked = env.Contains(name);
}
}
}
private void Persist()
{
var settings = new PerGameSettings
{
LogLevel = _logLevelRow.IsOverridden ? _logLevel.SelectedItem as string : null,
ImportTraceLimit = _traceRow.IsOverridden ? (int)(_trace.Value ?? 0) : null,
StrictDynlibResolution = _strictRow.IsOverridden ? _strict.IsChecked == true : null,
LogToFile = _logToFileRow.IsOverridden ? _logToFile.IsChecked == true : null,
EnvironmentToggles = _envRow.IsOverridden
? _envBoxes.Where(e => e.Box.IsChecked == true).Select(e => e.Name).ToList()
: null,
};
settings.Save(_titleId);
}
}
-101
View File
@@ -1,101 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Data;
using Avalonia.Media;
namespace SharpEmu.GUI;
public sealed class SettingRow : ContentControl
{
public static readonly StyledProperty<string?> LabelProperty =
AvaloniaProperty.Register<SettingRow, string?>(nameof(Label));
public static readonly StyledProperty<string?> DescriptionProperty =
AvaloniaProperty.Register<SettingRow, string?>(nameof(Description));
public static readonly StyledProperty<bool> ShowOverrideProperty =
AvaloniaProperty.Register<SettingRow, bool>(nameof(ShowOverride));
public static readonly StyledProperty<bool> IsOverriddenProperty =
AvaloniaProperty.Register<SettingRow, bool>(
nameof(IsOverridden), defaultBindingMode: BindingMode.TwoWay);
public static readonly StyledProperty<FontFamily?> LabelFontFamilyProperty =
AvaloniaProperty.Register<SettingRow, FontFamily?>(nameof(LabelFontFamily));
private ContentPresenter? _slot;
private TextBlock? _label;
public string? Label
{
get => GetValue(LabelProperty);
set => SetValue(LabelProperty, value);
}
public string? Description
{
get => GetValue(DescriptionProperty);
set => SetValue(DescriptionProperty, value);
}
public bool ShowOverride
{
get => GetValue(ShowOverrideProperty);
set => SetValue(ShowOverrideProperty, value);
}
public bool IsOverridden
{
get => GetValue(IsOverriddenProperty);
set => SetValue(IsOverriddenProperty, value);
}
public FontFamily? LabelFontFamily
{
get => GetValue(LabelFontFamilyProperty);
set => SetValue(LabelFontFamilyProperty, value);
}
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnApplyTemplate(e);
_slot = e.NameScope.Find<ContentPresenter>("PART_Slot");
_label = e.NameScope.Find<TextBlock>("PART_Label");
UpdateSlotEnabled();
UpdateLabelFont();
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
if (change.Property == ShowOverrideProperty || change.Property == IsOverriddenProperty)
{
UpdateSlotEnabled();
}
else if (change.Property == LabelFontFamilyProperty)
{
UpdateLabelFont();
}
}
private void UpdateLabelFont()
{
if (_label is not null && LabelFontFamily is { } family)
{
_label.FontFamily = family;
}
}
private void UpdateSlotEnabled()
{
if (_slot is not null)
{
_slot.IsEnabled = !ShowOverride || IsOverridden;
}
}
}
-1
View File
@@ -38,7 +38,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" /> <AvaloniaResource Include="..\..\assets\images\discord.png" Link="Assets/discord.png" />
<AvaloniaResource Include="..\..\assets\images\update-icon.png" Link="Assets/update-icon.png" /> <AvaloniaResource Include="..\..\assets\images\update-icon.png" Link="Assets/update-icon.png" />
<AvaloniaResource Include="..\..\assets\images\commit-icon.png" Link="Assets/commit-icon.png" /> <AvaloniaResource Include="..\..\assets\images\commit-icon.png" Link="Assets/commit-icon.png" />
<AvaloniaResource Include="..\..\assets\images\pic0.png" Link="Assets/pic0.png" />
</ItemGroup> </ItemGroup>
<ItemGroup> <ItemGroup>
+37 -206
View File
@@ -6,10 +6,7 @@ using System.Formats.Tar;
using System.IO.Compression; using System.IO.Compression;
using System.Net.Http.Headers; using System.Net.Http.Headers;
using System.Runtime.InteropServices; using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text.Json; using System.Text.Json;
using System.Text.RegularExpressions;
using System.Reflection;
namespace SharpEmu.GUI; namespace SharpEmu.GUI;
@@ -21,7 +18,7 @@ public static class Updater
private static readonly TimeSpan CheckTimeout = TimeSpan.FromSeconds(10); private static readonly TimeSpan CheckTimeout = TimeSpan.FromSeconds(10);
private static readonly HttpClient Http = CreateHttpClient(); private static readonly HttpClient Http = CreateHttpClient();
public sealed record UpdateInfo(string Sha, string Name, string DownloadUrl, long Size, string Sha256, string TagName); public sealed record UpdateInfo(string Sha, string Name, string DownloadUrl, long Size);
public static async Task<UpdateInfo?> CheckAsync(string? currentSha, CancellationToken cancellationToken = default) public static async Task<UpdateInfo?> CheckAsync(string? currentSha, CancellationToken cancellationToken = default)
{ {
@@ -31,31 +28,11 @@ public static class Updater
using var response = await Http.GetAsync(LatestReleaseUrl, timeout.Token); using var response = await Http.GetAsync(LatestReleaseUrl, timeout.Token);
response.EnsureSuccessStatusCode(); response.EnsureSuccessStatusCode();
var update = ParseRelease( return ParseRelease(
await response.Content.ReadAsStringAsync(timeout.Token), await response.Content.ReadAsStringAsync(timeout.Token),
null, currentSha,
platform.Rid, platform.Rid,
platform.Extension); platform.Extension);
var currentVersion = Assembly.GetExecutingAssembly()
.GetCustomAttribute<AssemblyInformationalVersionAttribute>()?.InformationalVersion;
if (update is null || currentSha is null ||
string.Equals(update.Sha, currentSha, StringComparison.OrdinalIgnoreCase))
{
return null;
}
if (currentVersion is not null &&
TryParseVersion(currentVersion, out var installed) &&
TryParseVersion(update.TagName, out var available) &&
available.CompareTo(installed) <= 0)
{
return null;
}
var comparison = await CompareCommitsAsync(currentSha, update.Sha, timeout.Token);
return comparison.Status == "ahead" && comparison.ReleaseDate > comparison.CurrentDate
? update
: null;
} }
public static async Task DownloadAndRestartAsync( public static async Task DownloadAndRestartAsync(
@@ -70,63 +47,42 @@ public static class Updater
Directory.Delete(root, recursive: true); Directory.Delete(root, recursive: true);
} }
var launched = false; Directory.CreateDirectory(root);
try var archive = Path.Combine(root, update.Name);
using (var response = await Http.GetAsync(update.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken))
{ {
Directory.CreateDirectory(root); response.EnsureSuccessStatusCode();
var archive = Path.Combine(root, update.Name); await using var input = await response.Content.ReadAsStreamAsync(cancellationToken);
using (var response = await Http.GetAsync(update.DownloadUrl, HttpCompletionOption.ResponseHeadersRead, cancellationToken)) await using var output = File.Create(archive);
var buffer = new byte[81920];
long written = 0;
int read;
while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0)
{ {
response.EnsureSuccessStatusCode(); await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
await using var input = await response.Content.ReadAsStreamAsync(cancellationToken); written += read;
await using var output = File.Create(archive); progress?.Report(update.Size == 0 ? 0 : (int)(written * 100 / update.Size));
var buffer = new byte[81920];
long written = 0;
int read;
while ((read = await input.ReadAsync(buffer, cancellationToken)) > 0)
{
await output.WriteAsync(buffer.AsMemory(0, read), cancellationToken);
written += read;
progress?.Report(update.Size == 0 ? 0 : (int)(written * 100 / update.Size));
}
if (written != update.Size)
{
throw new InvalidDataException($"Downloaded {written} bytes; expected {update.Size}.");
}
} }
await using (var archiveStream = File.OpenRead(archive)) if (written != update.Size)
{ {
var actualSha256 = Convert.ToHexString(await SHA256.HashDataAsync(archiveStream, cancellationToken)); throw new InvalidDataException($"Downloaded {written} bytes; expected {update.Size}.");
if (!string.Equals(actualSha256, update.Sha256, StringComparison.OrdinalIgnoreCase))
{
throw new InvalidDataException($"SHA-256 mismatch; expected {update.Sha256}, got {actualSha256}.");
}
}
var platform = CurrentPlatform();
var stagedExe = ExtractArchive(archive, payload, platform.Extension, platform.ExecutableName);
var start = new ProcessStartInfo(stagedExe)
{
UseShellExecute = false,
WorkingDirectory = payload,
};
start.ArgumentList.Add(ApplyArgument);
start.ArgumentList.Add(Environment.ProcessId.ToString());
start.ArgumentList.Add(AppContext.BaseDirectory);
using var helper = Process.Start(start)
?? throw new InvalidOperationException("The update installer could not be started.");
launched = true;
}
finally
{
if (!launched)
{
TryDeleteDirectory(root);
} }
} }
var platform = CurrentPlatform();
var stagedExe = ExtractArchive(archive, payload, platform.Extension, platform.ExecutableName);
var start = new ProcessStartInfo(stagedExe)
{
UseShellExecute = false,
WorkingDirectory = payload,
};
start.ArgumentList.Add(ApplyArgument);
start.ArgumentList.Add(Environment.ProcessId.ToString());
start.ArgumentList.Add(AppContext.BaseDirectory);
using var helper = Process.Start(start)
?? throw new InvalidOperationException("The update installer could not be started.");
} }
/// <summary>Runs from the downloaded executable after the old GUI exits.</summary> /// <summary>Runs from the downloaded executable after the old GUI exits.</summary>
@@ -138,8 +94,6 @@ public static class Updater
return false; return false;
} }
var backup = Path.Combine(Path.GetTempPath(), $"SharpEmu.UpdateBackup-{Environment.ProcessId}");
var changed = new List<(string Destination, string? Backup)>();
try try
{ {
if (int.TryParse(args[1], out var oldPid)) if (int.TryParse(args[1], out var oldPid))
@@ -159,7 +113,6 @@ public static class Updater
var source = AppContext.BaseDirectory; var source = AppContext.BaseDirectory;
var target = Path.GetFullPath(args[2]); var target = Path.GetFullPath(args[2]);
Directory.CreateDirectory(backup);
foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories)) foreach (var file in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
{ {
var relative = Path.GetRelativePath(source, file); var relative = Path.GetRelativePath(source, file);
@@ -173,14 +126,6 @@ public static class Updater
var destination = Path.Combine(target, relative); var destination = Path.Combine(target, relative);
Directory.CreateDirectory(Path.GetDirectoryName(destination)!); Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
string? backupFile = null;
if (File.Exists(destination))
{
backupFile = Path.Combine(backup, relative);
Directory.CreateDirectory(Path.GetDirectoryName(backupFile)!);
File.Copy(destination, backupFile, overwrite: true);
}
changed.Add((destination, backupFile));
File.Copy(file, destination, overwrite: true); File.Copy(file, destination, overwrite: true);
if (!OperatingSystem.IsWindows()) if (!OperatingSystem.IsWindows())
{ {
@@ -194,31 +139,10 @@ public static class Updater
UseShellExecute = false, UseShellExecute = false,
WorkingDirectory = target, WorkingDirectory = target,
}) ?? throw new InvalidOperationException("The updated SharpEmu could not be started."); }) ?? throw new InvalidOperationException("The updated SharpEmu could not be started.");
TryDeleteDirectory(backup);
} }
catch (Exception ex) catch (Exception ex)
{ {
exitCode = 1; exitCode = 1;
foreach (var (destination, backupFile) in changed.AsEnumerable().Reverse())
{
try
{
if (backupFile is null)
{
File.Delete(destination);
}
else if (File.Exists(backupFile))
{
Directory.CreateDirectory(Path.GetDirectoryName(destination)!);
File.Copy(backupFile, destination, overwrite: true);
}
}
catch
{
// Best-effort rollback; the original error is more useful to the user.
}
}
TryDeleteDirectory(backup);
try try
{ {
File.WriteAllText(Path.Combine(args[2], "update-error.log"), ex.ToString()); File.WriteAllText(Path.Combine(args[2], "update-error.log"), ex.ToString());
@@ -239,12 +163,11 @@ public static class Updater
string extension) string extension)
{ {
using var document = JsonDocument.Parse(json); using var document = JsonDocument.Parse(json);
var releaseSha = ExtractReleaseSha(document.RootElement);
var candidates = new List<(DateTimeOffset Created, UpdateInfo Update)>(); var candidates = new List<(DateTimeOffset Created, UpdateInfo Update)>();
foreach (var asset in document.RootElement.GetProperty("assets").EnumerateArray()) foreach (var asset in document.RootElement.GetProperty("assets").EnumerateArray())
{ {
var name = asset.GetProperty("name").GetString() ?? ""; var name = asset.GetProperty("name").GetString() ?? "";
var marker = $"-{rid}"; var marker = $"-{rid}-";
var markerIndex = name.LastIndexOf(marker, StringComparison.OrdinalIgnoreCase); var markerIndex = name.LastIndexOf(marker, StringComparison.OrdinalIgnoreCase);
if (!name.EndsWith(extension, StringComparison.OrdinalIgnoreCase) || if (!name.EndsWith(extension, StringComparison.OrdinalIgnoreCase) ||
markerIndex < 0) markerIndex < 0)
@@ -252,21 +175,8 @@ public static class Updater
continue; continue;
} }
var suffix = name[(markerIndex + marker.Length)..^extension.Length].TrimStart('-'); var sha = name[(markerIndex + marker.Length)..^extension.Length];
var assetSha = suffix.Length >= 7 && suffix.All(Uri.IsHexDigit) if (sha.Length < 7 || !sha.All(Uri.IsHexDigit))
? suffix
: releaseSha;
if (assetSha is null ||
!asset.TryGetProperty("digest", out var digestProperty) ||
digestProperty.ValueKind != JsonValueKind.String)
{
continue;
}
var digest = digestProperty.GetString() ?? "";
if (!digest.StartsWith("sha256:", StringComparison.OrdinalIgnoreCase) ||
digest.Length != "sha256:".Length + 64 ||
!digest["sha256:".Length..].All(Uri.IsHexDigit))
{ {
continue; continue;
} }
@@ -274,12 +184,10 @@ public static class Updater
candidates.Add(( candidates.Add((
asset.GetProperty("created_at").GetDateTimeOffset(), asset.GetProperty("created_at").GetDateTimeOffset(),
new UpdateInfo( new UpdateInfo(
assetSha, sha,
name, name,
asset.GetProperty("browser_download_url").GetString()!, asset.GetProperty("browser_download_url").GetString()!,
asset.GetProperty("size").GetInt64(), asset.GetProperty("size").GetInt64())));
digest["sha256:".Length..],
document.RootElement.GetProperty("tag_name").GetString() ?? "")));
} }
var latest = candidates.OrderByDescending(candidate => candidate.Created).FirstOrDefault().Update; var latest = candidates.OrderByDescending(candidate => candidate.Created).FirstOrDefault().Update;
@@ -288,73 +196,6 @@ public static class Updater
: latest; : latest;
} }
private static async Task<CommitComparison> CompareCommitsAsync(
string currentSha,
string releaseSha,
CancellationToken cancellationToken)
{
var url = $"https://api.github.com/repos/sharpemu/sharpemu/compare/{currentSha}...{releaseSha}";
using var response = await Http.GetAsync(url, cancellationToken);
response.EnsureSuccessStatusCode();
using var document = JsonDocument.Parse(await response.Content.ReadAsStringAsync(cancellationToken));
var root = document.RootElement;
var currentDate = root.GetProperty("base_commit").GetProperty("commit").GetProperty("committer").GetProperty("date").GetDateTimeOffset();
var releaseDate = currentDate;
if (root.TryGetProperty("commits", out var commits) && commits.GetArrayLength() > 0)
{
releaseDate = commits[commits.GetArrayLength() - 1]
.GetProperty("commit").GetProperty("committer").GetProperty("date").GetDateTimeOffset();
}
return new CommitComparison(root.GetProperty("status").GetString() ?? "", currentDate, releaseDate);
}
private static string? ExtractReleaseSha(JsonElement release)
{
if (!release.TryGetProperty("body", out var bodyProperty) ||
bodyProperty.ValueKind != JsonValueKind.String)
{
return null;
}
var body = bodyProperty.GetString();
var match = Regex.Match(
body ?? "",
@"\bcommit\s+([0-9a-f]{7,40})\b",
RegexOptions.IgnoreCase | RegexOptions.CultureInvariant);
if (!match.Success)
{
return null;
}
var sha = match.Groups[1].Value;
return sha.Length > 7 ? sha[..7] : sha;
}
private static bool TryParseVersion(string value, out ReleaseVersion version)
{
var match = Regex.Match(value.TrimStart('v'), @"^(\d+)\.(\d+)\.(\d+)(?:-([0-9A-Za-z.-]+))?");
if (!match.Success || !int.TryParse(match.Groups[1].Value, out var major) ||
!int.TryParse(match.Groups[2].Value, out var minor) ||
!int.TryParse(match.Groups[3].Value, out var patch))
{
version = default;
return false;
}
version = new ReleaseVersion(major, minor, patch, match.Groups[4].Value);
return true;
}
private static void TryDeleteDirectory(string path)
{
try
{
if (Directory.Exists(path)) Directory.Delete(path, recursive: true);
}
catch { }
}
private static string ExtractArchive( private static string ExtractArchive(
string archive, string archive,
string payload, string payload,
@@ -409,14 +250,4 @@ public static class Updater
} }
private sealed record PlatformInfo(string Rid, string Extension, string ExecutableName); private sealed record PlatformInfo(string Rid, string Extension, string ExecutableName);
private sealed record CommitComparison(string Status, DateTimeOffset CurrentDate, DateTimeOffset ReleaseDate);
private readonly record struct ReleaseVersion(int Major, int Minor, int Patch, string PreRelease) : IComparable<ReleaseVersion>
{
public int CompareTo(ReleaseVersion other) =>
(Major, Minor, Patch) != (other.Major, other.Minor, other.Patch)
? (Major, Minor, Patch).CompareTo((other.Major, other.Minor, other.Patch))
: string.IsNullOrEmpty(PreRelease) == string.IsNullOrEmpty(other.PreRelease)
? string.CompareOrdinal(PreRelease, other.PreRelease)
: string.IsNullOrEmpty(PreRelease) ? 1 : -1;
}
} }
+5 -40
View File
@@ -51,33 +51,9 @@ public static unsafe class GuestImageWriteTracker
private static readonly object _gate = new(); private static readonly object _gate = new();
private static readonly Dictionary<ulong, TrackedRange> _rangesByAddress = new(); private static readonly Dictionary<ulong, TrackedRange> _rangesByAddress = new();
/// <summary>Immutable snapshot read lock-free from the signal handler and // Snapshot array read lock-free from the signal handler; rebuilt on every
/// the managed-write pre-visit; rebuilt on every mutation under the gate // mutation under the gate. Signal handlers must not take managed locks.
/// (signal handlers must not take managed locks). Carrying the overall private static TrackedRange[] _rangeSnapshot = [];
/// bounds inside the same object keeps the hot-path intersection test
/// consistent with the array it guards.</summary>
private sealed class RangeSnapshot
{
public static readonly RangeSnapshot Empty = new([]);
public readonly TrackedRange[] Ranges;
public readonly ulong Start;
public readonly ulong End;
public RangeSnapshot(TrackedRange[] ranges)
{
Ranges = ranges;
Start = ulong.MaxValue;
End = 0;
foreach (var range in ranges)
{
Start = Math.Min(Start, range.Start);
End = Math.Max(End, range.End);
}
}
}
private static RangeSnapshot _rangeSnapshot = RangeSnapshot.Empty;
private static readonly bool _enabled = !OperatingSystem.IsWindows() && private static readonly bool _enabled = !OperatingSystem.IsWindows() &&
Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0"; Environment.GetEnvironmentVariable("SHARPEMU_GUEST_IMAGE_CPU_SYNC") != "0";
@@ -290,17 +266,6 @@ public static unsafe class GuestImageWriteTracker
var end = address > ulong.MaxValue - byteCount var end = address > ulong.MaxValue - byteCount
? ulong.MaxValue ? ulong.MaxValue
: address + byteCount; : address + byteCount;
// Fast rejection for the hot path: this runs on every managed guest
// write, and almost none of them touch tracked texture pages. The
// bounds live inside the snapshot so they are always consistent with
// the ranges the per-page visit below would consult.
var snapshot = Volatile.Read(ref _rangeSnapshot);
if (snapshot.Ranges.Length == 0 || end <= snapshot.Start || address >= snapshot.End)
{
return;
}
var candidate = address; var candidate = address;
while (candidate < end) while (candidate < end)
{ {
@@ -346,7 +311,7 @@ public static unsafe class GuestImageWriteTracker
return false; return false;
} }
var ranges = Volatile.Read(ref _rangeSnapshot).Ranges; var ranges = Volatile.Read(ref _rangeSnapshot);
var writableStart = ulong.MaxValue; var writableStart = ulong.MaxValue;
var writableEnd = 0UL; var writableEnd = 0UL;
for (var index = 0; index < ranges.Length; index++) for (var index = 0; index < ranges.Length; index++)
@@ -493,7 +458,7 @@ public static unsafe class GuestImageWriteTracker
private static void RebuildSnapshotLocked() private static void RebuildSnapshotLocked()
{ {
Volatile.Write(ref _rangeSnapshot, new RangeSnapshot(_rangesByAddress.Values.ToArray())); _rangeSnapshot = _rangesByAddress.Values.ToArray();
} }
private static (ulong Start, ulong Length) PageAlign(ulong address, ulong byteCount) private static (ulong Start, ulong Length) PageAlign(ulong address, ulong byteCount)
+21
View File
@@ -398,6 +398,27 @@ public static class GuestThreadExecution
return true; return true;
} }
public static bool TryConsumeCurrentThreadBlock(
out string reason,
out GuestCpuContinuation continuation,
out bool hasContinuation,
out string wakeKey,
out Func<int>? resumeHandler,
out Func<bool>? wakeHandler,
out long blockDeadlineTimestamp)
{
var consumed = TryConsumeCurrentThreadBlock(
out reason,
out continuation,
out hasContinuation,
out wakeKey,
out var waiter,
out blockDeadlineTimestamp);
resumeHandler = waiter is null ? null : waiter.Resume;
wakeHandler = waiter is null ? null : waiter.TryWake;
return consumed;
}
public static long ComputeDeadlineTimestamp(TimeSpan timeout) public static long ComputeDeadlineTimestamp(TimeSpan timeout)
{ {
if (timeout <= TimeSpan.Zero) if (timeout <= TimeSpan.Zero)
+120 -361
View File
@@ -14,12 +14,6 @@ namespace SharpEmu.Libs.Agc;
public static partial class AgcExports public static partial class AgcExports
{ {
// The backend is a process-fixed singleton, so its offset-alignment
// requirement is snapshot once: several per-draw paths (shader-key
// hashing, buffer-offset alignment) read it in loops.
private static readonly ulong _storageBufferOffsetAlignment =
GuestGpu.Current.GuestStorageBufferOffsetAlignment;
#if DEBUG #if DEBUG
static AgcExports() static AgcExports()
{ {
@@ -568,8 +562,6 @@ public static partial class AgcExports
public ulong WorkSequence { get; set; } public ulong WorkSequence { get; set; }
public ulong SubmissionSequence { get; set; } public ulong SubmissionSequence { get; set; }
public bool WaitMonitorRunning { get; set; } public bool WaitMonitorRunning { get; set; }
public object WaitMonitorSignalGate { get; } = new();
public long WaitMonitorSignalVersion { get; set; }
} }
private readonly record struct RegisteredAgcResource( private readonly record struct RegisteredAgcResource(
@@ -2674,7 +2666,7 @@ public static partial class AgcExports
TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}"); TraceAgc($"agc.driver_submit_dcb packet=0x{packetAddress:X16} addr=0x{commandAddress:X16} dwords={dwordCount}");
} }
GuestGpu.Current.AttachGuestMemory(ctx.Memory); VulkanVideoPresenter.AttachGuestMemory(ctx.Memory);
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (gpuState.Gate) lock (gpuState.Gate)
{ {
@@ -2726,7 +2718,7 @@ public static partial class AgcExports
$"addr=0x{commandAddress:X16} dwords={dwordCount}"); $"addr=0x{commandAddress:X16} dwords={dwordCount}");
} }
GuestGpu.Current.AttachGuestMemory(ctx.Memory); VulkanVideoPresenter.AttachGuestMemory(ctx.Memory);
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState()); var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
lock (gpuState.Gate) lock (gpuState.Gate)
{ {
@@ -2954,7 +2946,7 @@ public static partial class AgcExports
// guest-memory writes have finished. Put the notification on that same // guest-memory writes have finished. Put the notification on that same
// logical graphics queue instead of approximating completion with a // logical graphics queue instead of approximating completion with a
// timer, which can wake Unity while its upload data is still stale. // timer, which can wake Unity while its upload data is still stale.
if (GuestGpu.Current.SubmitOrderedGuestAction( if (VulkanVideoPresenter.SubmitOrderedGuestAction(
TriggerCompletionEvents, TriggerCompletionEvents,
$"agc submit completion {submissionId}") == 0) $"agc submit completion {submissionId}") == 0)
{ {
@@ -2978,11 +2970,11 @@ public static partial class AgcExports
return false; return false;
} }
using var guestQueueScope = GuestGpu.Current.EnterGuestQueue( using var guestQueueScope = VulkanVideoPresenter.EnterGuestQueue(
state.QueueName, state.QueueName,
state.ActiveSubmissionId); state.ActiveSubmissionId);
var windowByteCount = checked((int)(dwordCount * sizeof(uint))); var windowByteCount = checked((int)(dwordCount * sizeof(uint)));
var rented = GuestDataPool.Shared.Rent(windowByteCount); var rented = VulkanVideoPresenter.GuestDataPool.Rent(windowByteCount);
try try
{ {
if (ctx.Memory.TryRead(commandAddress, rented.AsSpan(0, windowByteCount))) if (ctx.Memory.TryRead(commandAddress, rented.AsSpan(0, windowByteCount)))
@@ -3004,7 +2996,7 @@ public static partial class AgcExports
{ {
_dcbWindowBuffer = null; _dcbWindowBuffer = null;
_dcbWindowByteLength = 0; _dcbWindowByteLength = 0;
GuestDataPool.Shared.Return(rented); VulkanVideoPresenter.GuestDataPool.Return(rented);
} }
} }
@@ -3306,20 +3298,17 @@ public static partial class AgcExports
indexed: false); indexed: false);
} }
if (op is ItDispatchDirect or ItDispatchIndirect) if ((op is ItDispatchDirect or ItDispatchIndirect) &&
TryReadComputeDispatch(
ctx,
state,
currentAddress,
length,
op,
out var dispatch))
{ {
if (TryReadComputeDispatch( state.FrameDispatchCount++;
ctx, ObserveComputeDispatch(ctx, gpuState, state, dispatch);
state,
currentAddress,
length,
op,
out var dispatch,
out _))
{
state.FrameDispatchCount++;
ObserveComputeDispatch(ctx, gpuState, state, dispatch);
}
} }
if (op == ItNop && if (op == ItNop &&
@@ -3328,7 +3317,7 @@ public static partial class AgcExports
TryReadUInt32(ctx, currentAddress + 4, out var waitVideoOutHandle) && TryReadUInt32(ctx, currentAddress + 4, out var waitVideoOutHandle) &&
TryReadUInt32(ctx, currentAddress + 8, out var waitDisplayBufferIndex)) TryReadUInt32(ctx, currentAddress + 8, out var waitDisplayBufferIndex))
{ {
var waitSequence = GuestGpu.Current.SubmitOrderedGuestFlipWait( var waitSequence = VulkanVideoPresenter.SubmitOrderedGuestFlipWait(
unchecked((int)waitVideoOutHandle), unchecked((int)waitVideoOutHandle),
unchecked((int)waitDisplayBufferIndex)); unchecked((int)waitDisplayBufferIndex));
TraceAgcShader( TraceAgcShader(
@@ -3657,11 +3646,27 @@ public static partial class AgcExports
void CompleteAndWake() void CompleteAndWake()
{ {
CompleteLabelProducer(producer); CompleteLabelProducer(producer);
lock (gpuState.WaitMonitorSignalGate) if (GpuWaitRegistry.Count == 0)
{ {
gpuState.WaitMonitorSignalVersion++; return;
Monitor.Pulse(gpuState.WaitMonitorSignalGate);
} }
// Resuming a DCB can enqueue another compute dispatch and wait for
// it. Never do that reentrantly on the Vulkan render thread.
ThreadPool.UnsafeQueueUserWorkItem(
static state =>
{
var (resumeContext, resumeGpuState) = state;
lock (resumeGpuState.Gate)
{
DrainResumableDcbs(
resumeContext,
resumeGpuState,
tracePackets: _traceAgc);
}
},
(ctx, gpuState),
preferLocal: false);
} }
void ApplyAndQueueCompletion() void ApplyAndQueueCompletion()
@@ -3672,7 +3677,7 @@ public static partial class AgcExports
// wake another queue before that mirror is visible. Queue a // wake another queue before that mirror is visible. Queue a
// second same-queue ordered action after all immediate follow-up // second same-queue ordered action after all immediate follow-up
// writes; it fences those writes before publishing the producer. // writes; it fences those writes before publishing the producer.
if (GuestGpu.Current.SubmitOrderedGuestAction( if (VulkanVideoPresenter.SubmitOrderedGuestAction(
CompleteAndWake, CompleteAndWake,
$"{debugName} completion") == 0) $"{debugName} completion") == 0)
{ {
@@ -3680,7 +3685,7 @@ public static partial class AgcExports
} }
} }
if (GuestGpu.Current.SubmitOrderedGuestAction( if (VulkanVideoPresenter.SubmitOrderedGuestAction(
ApplyAndQueueCompletion, ApplyAndQueueCompletion,
debugName) == 0) debugName) == 0)
{ {
@@ -3890,11 +3895,11 @@ public static partial class AgcExports
TraceAgc( TraceAgc(
$"agc.acquire_mem_applied queue={queueName} " + $"agc.acquire_mem_applied queue={queueName} " +
$"submission={submissionId} packet=0x{packetAddress:X16} " + $"submission={submissionId} packet=0x{packetAddress:X16} " +
$"work_sequence={GuestGpu.Current.CurrentGuestWorkSequenceForDiagnostics}"); $"work_sequence={VulkanVideoPresenter.CurrentGuestWorkSequenceForDiagnostics}");
} }
} }
var sequence = GuestGpu.Current.SubmitOrderedGuestAction( var sequence = VulkanVideoPresenter.SubmitOrderedGuestAction(
ApplyAcquire, ApplyAcquire,
debugName); debugName);
if (sequence == 0) if (sequence == 0)
@@ -4040,7 +4045,7 @@ public static partial class AgcExports
return; return;
} }
foreach (var (address, width, height, byteCount) in GuestGpu.Current.GetGuestImageExtents()) foreach (var (address, width, height, byteCount) in VulkanVideoPresenter.GetGuestImageExtents())
{ {
if (scopeByteCount != ulong.MaxValue && if (scopeByteCount != ulong.MaxValue &&
!RangesOverlap(address, byteCount, scopeAddress, scopeByteCount)) !RangesOverlap(address, byteCount, scopeAddress, scopeByteCount))
@@ -4061,7 +4066,7 @@ public static partial class AgcExports
var pixels = new byte[byteCount]; var pixels = new byte[byteCount];
if (ctx.Memory.TryRead(address, pixels)) if (ctx.Memory.TryRead(address, pixels))
{ {
GuestGpu.Current.SubmitGuestImageWrite(address, pixels); VulkanVideoPresenter.SubmitGuestImageWrite(address, pixels);
if (Interlocked.Increment(ref _guestImageSyncTraceCount) <= 64) if (Interlocked.Increment(ref _guestImageSyncTraceCount) <= 64)
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
@@ -4104,7 +4109,7 @@ public static partial class AgcExports
ulong byteCount, ulong byteCount,
uint? fillValue) uint? fillValue)
{ {
var hasImage = GuestGpu.Current.TryGetGuestImageExtent( var hasImage = VulkanVideoPresenter.TryGetGuestImageExtent(
destinationAddress, destinationAddress,
out var width, out var width,
out var height, out var height,
@@ -4128,14 +4133,14 @@ public static partial class AgcExports
if (fillValue is { } fill) if (fillValue is { } fill)
{ {
GuestGpu.Current.SubmitGuestImageFill(destinationAddress, fill); VulkanVideoPresenter.SubmitGuestImageFill(destinationAddress, fill);
return; return;
} }
var pixels = new byte[imageBytes]; var pixels = new byte[imageBytes];
if (ctx.Memory.TryRead(destinationAddress, pixels)) if (ctx.Memory.TryRead(destinationAddress, pixels))
{ {
GuestGpu.Current.SubmitGuestImageWrite(destinationAddress, pixels); VulkanVideoPresenter.SubmitGuestImageWrite(destinationAddress, pixels);
} }
} }
@@ -4523,17 +4528,6 @@ public static partial class AgcExports
? fallbackMs ? fallbackMs
: 0L) * System.Diagnostics.Stopwatch.Frequency / 1000L; : 0L) * System.Diagnostics.Stopwatch.Frequency / 1000L;
// How long a suspended GPU wait may sit before the deadlock breaker may
// release it using the last value a real producer wrote to its label. Long
// enough that legitimate GPU work (which completes within a frame) never
// trips it; short enough that a wedged cross-queue cycle unblocks quickly.
private static readonly long _gpuDeadlockBreakTicks =
(long.TryParse(
Environment.GetEnvironmentVariable("SHARPEMU_GPU_DEADLOCK_BREAK_MS"),
out var deadlockMs) && deadlockMs > 0
? deadlockMs
: 500L) * System.Diagnostics.Stopwatch.Frequency / 1000L;
// Reads the WAIT_REG_MEM watched address, reference, mask, and 3-bit compare // Reads the WAIT_REG_MEM watched address, reference, mask, and 3-bit compare
// function for both the AGC NOP-encapsulated (RWaitMem32/64) and the standard // function for both the AGC NOP-encapsulated (RWaitMem32/64) and the standard
// ItWaitRegMem packet layouts. // ItWaitRegMem packet layouts.
@@ -4598,85 +4592,6 @@ public static partial class AgcExports
// Returns true when the DCB should suspend parsing at this wait (its // Returns true when the DCB should suspend parsing at this wait (its
// continuation was registered into GpuWaitRegistry); false to keep parsing // continuation was registered into GpuWaitRegistry); false to keep parsing
// (already satisfied, unreadable, or legacy force-satisfy mode). // (already satisfied, unreadable, or legacy force-satisfy mode).
// How long an indirect dispatch may wait for its producing dispatch to write
// non-zero dimensions before we give up and drop it (matching the pre-existing
// reject behavior). The producer runs on the render thread within a frame or
// two; this only bounds the pathological/legitimately-empty case.
private const long IndirectDimsRetryBudgetMs = 150;
private static readonly object _indirectDimsGate = new();
// Keys (memory, packetAddress) whose retry deadline elapsed. Added by
// DrainResumableDcbs when it resumes an expired retry, consumed by the very
// next re-parse of that packet so it drops instead of re-suspending. Never
// persists across frames — a fresh submit of the same packet retries anew.
private static readonly HashSet<(object, ulong)> _indirectDimsExpired = new();
// Suspends an indirect-dispatch DCB until the guest buffer holding its
// thread-group dimensions becomes non-zero (written by a prior GPU dispatch),
// then re-parses the dispatch. Returns false — so the caller drops the work —
// when the dims already expired once (genuinely empty dispatch).
private static bool HandleSubmittedIndirectDimsWait(
CpuContext ctx,
SubmittedDcbState state,
ulong commandAddress,
ulong packetAddress,
uint offset,
uint dwordCount,
ulong dimsAddress,
bool tracePacket)
{
if (!_gpuWaitSuspendEnabled ||
dimsAddress == 0 ||
dimsAddress % sizeof(uint) != 0)
{
return false;
}
var key = (ctx.Memory, packetAddress);
lock (_indirectDimsGate)
{
// This is the re-parse right after the deadline elapsed: drop the
// dispatch instead of suspending again.
if (_indirectDimsExpired.Remove(key))
{
return false;
}
}
var waiter = new GpuWaitRegistry.WaitingDcb
{
CommandBufferAddress = commandAddress,
ResumeAddress = packetAddress, // re-parse this dispatch packet
ResumeOffset = offset,
TotalDwords = dwordCount,
WaitAddress = dimsAddress,
ReferenceValue = 0,
Mask = 0xFFFFFFFF,
CompareFunction = 4, // NOT_EQUAL: dims became available
Is64Bit = false,
IsStandard = false,
Memory = ctx.Memory,
QueueName = state.QueueName,
SubmissionId = state.ActiveSubmissionId,
RegisteredTicks = System.Diagnostics.Stopwatch.GetTimestamp(),
RetryDeadlineTicks = System.Diagnostics.Stopwatch.GetTimestamp() +
(IndirectDimsRetryBudgetMs * System.Diagnostics.Stopwatch.Frequency / 1000L),
State = state,
};
GpuWaitRegistry.Register(dimsAddress, waiter);
var gpuState = _submittedGpuStates.GetValue(ctx.Memory, static _ => new SubmittedGpuState());
EnsureGpuWaitMonitor(ctx, gpuState);
if (tracePacket)
{
TraceAgc(
$"agc.dispatch_indirect_wait dims=0x{dimsAddress:X16} " +
$"packet=0x{packetAddress:X16} queue={state.QueueName}");
}
return true;
}
private static bool HandleSubmittedWaitRegMem( private static bool HandleSubmittedWaitRegMem(
CpuContext ctx, CpuContext ctx,
SubmittedDcbState state, SubmittedDcbState state,
@@ -4840,45 +4755,38 @@ public static partial class AgcExports
SubmittedGpuState gpuState) SubmittedGpuState gpuState)
{ {
var delayMilliseconds = 1; var delayMilliseconds = 1;
long observedSignal;
lock (gpuState.WaitMonitorSignalGate)
{
observedSignal = gpuState.WaitMonitorSignalVersion;
}
while (true) while (true)
{ {
int resumed; var madeProgress = false;
int remaining;
lock (gpuState.Gate) lock (gpuState.Gate)
{ {
resumed = DrainResumableDcbs(ctx, gpuState, tracePackets: _traceAgc); var before = GpuWaitRegistry.CountForMemory(ctx.Memory);
remaining = GpuWaitRegistry.CountForMemory(ctx.Memory); if (before == 0)
if (_traceAgc && resumed != 0) {
gpuState.WaitMonitorRunning = false;
return;
}
DrainResumableDcbs(ctx, gpuState, tracePackets: _traceAgc);
var after = GpuWaitRegistry.CountForMemory(ctx.Memory);
madeProgress = after < before;
if (madeProgress)
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
$"[LOADER][TRACE] agc.wait_monitor_resumed count={resumed} " + $"[LOADER][TRACE] agc.wait_monitor_resumed count={before - after} " +
$"remaining={remaining}"); $"remaining={after}");
} }
if (remaining == 0) if (after == 0)
{ {
gpuState.WaitMonitorRunning = false; gpuState.WaitMonitorRunning = false;
return; return;
} }
} }
delayMilliseconds = resumed != 0 delayMilliseconds = madeProgress
? 1 ? 1
: Math.Min(delayMilliseconds * 2, 16); : Math.Min(delayMilliseconds * 2, 16);
lock (gpuState.WaitMonitorSignalGate) Thread.Sleep(delayMilliseconds);
{
if (gpuState.WaitMonitorSignalVersion == observedSignal)
{
Monitor.Wait(gpuState.WaitMonitorSignalGate, delayMilliseconds);
}
observedSignal = gpuState.WaitMonitorSignalVersion;
}
} }
} }
@@ -4932,17 +4840,16 @@ public static partial class AgcExports
// guest memory (labels are advanced by ReleaseMem/WriteData/DmaData packets // guest memory (labels are advanced by ReleaseMem/WriteData/DmaData packets
// or direct CPU writes) and resumes the ones now satisfied. A resumed DCB // or direct CPU writes) and resumes the ones now satisfied. A resumed DCB
// can itself write labels that unblock others, so loop to a fixed point. // can itself write labels that unblock others, so loop to a fixed point.
private static int DrainResumableDcbs( private static void DrainResumableDcbs(
CpuContext ctx, CpuContext ctx,
SubmittedGpuState gpuState, SubmittedGpuState gpuState,
bool tracePackets) bool tracePackets)
{ {
if (!_gpuWaitSuspendEnabled) if (!_gpuWaitSuspendEnabled)
{ {
return 0; return;
} }
var resumedCount = 0;
for (var pass = 0; pass < 256; pass++) for (var pass = 0; pass < 256; pass++)
{ {
var woken = GpuWaitRegistry.CollectSatisfied(ctx.Memory, (address, is64Bit) => var woken = GpuWaitRegistry.CollectSatisfied(ctx.Memory, (address, is64Bit) =>
@@ -4950,50 +4857,7 @@ public static partial class AgcExports
? TryReadUInt64(ctx, address, out var value64) ? value64 : (ulong?)null ? TryReadUInt64(ctx, address, out var value64) ? value64 : (ulong?)null
: TryReadUInt32(ctx, address, out var value32) ? value32 : (ulong?)null); : TryReadUInt32(ctx, address, out var value32) ? value32 : (ulong?)null);
// Indirect-dispatch dimension retries whose deadline elapsed are if (woken is null)
// resumed so they drop instead of stalling. Flag each so its immediate
// re-parse drops the dispatch rather than suspending again.
var expiredRetries = GpuWaitRegistry.CollectExpiredRetries(
ctx.Memory, System.Diagnostics.Stopwatch.GetTimestamp());
if (expiredRetries is not null)
{
lock (_indirectDimsGate)
{
foreach (var retry in expiredRetries)
{
_indirectDimsExpired.Add((ctx.Memory, retry.ResumeAddress));
}
}
foreach (var retry in expiredRetries)
{
ResumeSuspendedDcb(ctx, gpuState, retry, tracePackets);
}
}
// Break cross-queue deadlocks: a waiter stuck past the deadline whose
// label a real producer already signalled (but guest memory has since
// been reset for reuse) is released using that produced value. Only
// fires for genuinely wedged waits, so fast-resolving ones on working
// titles are untouched.
var deadlockBroken = GpuWaitRegistry.CollectDeadlockBroken(
ctx.Memory, System.Diagnostics.Stopwatch.GetTimestamp(), _gpuDeadlockBreakTicks);
if (deadlockBroken is not null)
{
foreach (var waiter in deadlockBroken)
{
if (tracePackets)
{
TraceAgc(
$"agc.deadlock_break label=0x{waiter.WaitAddress:X16} " +
$"queue={waiter.QueueName} submission={waiter.SubmissionId}");
}
ResumeSuspendedDcb(ctx, gpuState, waiter, tracePackets);
}
}
if (woken is null && expiredRetries is null && deadlockBroken is null)
{ {
if (_gpuWaitStaleTicks > 0 && if (_gpuWaitStaleTicks > 0 &&
GpuWaitRegistry.CollectUnreportedStale( GpuWaitRegistry.CollectUnreportedStale(
@@ -5020,20 +4884,14 @@ public static partial class AgcExports
} }
} }
return resumedCount; return;
} }
if (woken is not null) foreach (var waiter in woken)
{ {
foreach (var waiter in woken) ResumeSuspendedDcb(ctx, gpuState, waiter, tracePackets);
{
ResumeSuspendedDcb(ctx, gpuState, waiter, tracePackets);
resumedCount++;
}
} }
} }
return resumedCount;
} }
private static void ResumeSuspendedDcb( private static void ResumeSuspendedDcb(
@@ -5175,15 +5033,6 @@ public static partial class AgcExports
_ => false, _ => false,
}); });
// Record + latch the written value so a same-frame label reset
// cannot lose the wakeup, and so the deadlock breaker can release
// a cross-queue waiter later (see ApplySubmittedReleaseMem).
if (wroteData && dataSelection is 1 or 2)
{
GpuWaitRegistry.RecordProduced(
ctx.Memory, destinationAddress, dataSelection == 1 ? dataLo : data);
}
if (tracePacket) if (tracePacket)
{ {
TraceAgc( TraceAgc(
@@ -5249,16 +5098,6 @@ public static partial class AgcExports
_ => false, _ => false,
}; };
// Latch waiters against the value we just wrote: the guest reuses
// these labels and can reset them to 0 before the wake pass reads
// memory, which otherwise loses the wakeup and stalls at a black
// screen (Astro Bot: graphics queue waiting on a compute EOP label).
if (wroteData && dataSelection is 1 or 2)
{
GpuWaitRegistry.RecordProduced(
ctx.Memory, destinationAddress, dataSelection == 1 ? dataLo : data);
}
if (tracePacket) if (tracePacket)
{ {
TraceAgc( TraceAgc(
@@ -5559,7 +5398,7 @@ public static partial class AgcExports
state.KnownRenderTargets[resolveSource.Address] = resolveSource; state.KnownRenderTargets[resolveSource.Address] = resolveSource;
state.KnownRenderTargets[resolveDestination.Address] = resolveDestination; state.KnownRenderTargets[resolveDestination.Address] = resolveDestination;
ProvideRenderTargetInitialData(ctx, resolveSource); ProvideRenderTargetInitialData(ctx, resolveSource);
if (GuestGpu.Current.TrySubmitGuestImageBlit( if (VulkanVideoPresenter.TrySubmitGuestImageBlit(
resolveSource.Address, resolveSource.Address,
resolveSource.Width, resolveSource.Width,
resolveSource.Height, resolveSource.Height,
@@ -5870,7 +5709,7 @@ public static partial class AgcExports
var cacheKey = ( var cacheKey = (
exportShaderAddress, exportShaderAddress,
exportFingerprint, exportFingerprint,
_storageBufferOffsetAlignment); VulkanVideoPresenter.GuestStorageBufferOffsetAlignment);
_depthOnlyVertexShaderCache.TryGetValue(cacheKey, out var vertexShader); _depthOnlyVertexShaderCache.TryGetValue(cacheKey, out var vertexShader);
if (vertexShader is null) if (vertexShader is null)
@@ -5895,7 +5734,7 @@ public static partial class AgcExports
: guestGlobalBufferCount + 1, : guestGlobalBufferCount + 1,
requiredVertexOutputCount: 0, requiredVertexOutputCount: 0,
storageBufferOffsetAlignment: storageBufferOffsetAlignment:
_storageBufferOffsetAlignment)) VulkanVideoPresenter.GuestStorageBufferOffsetAlignment))
{ {
ReturnPooledEvaluationArrays(exportEvaluation); ReturnPooledEvaluationArrays(exportEvaluation);
return false; return false;
@@ -5907,7 +5746,7 @@ public static partial class AgcExports
exportFingerprint, exportFingerprint,
vertexShader!, vertexShader!,
exportState.Program); exportState.Program);
GuestGpu.Current.CountShaderCompilation(); VulkanVideoPresenter.CountSpirvCompilation();
_depthOnlyVertexShaderCache.TryAdd(cacheKey, vertexShader!); _depthOnlyVertexShaderCache.TryAdd(cacheKey, vertexShader!);
} }
@@ -5940,9 +5779,7 @@ public static partial class AgcExports
textures.Add(new TranslatedImageBinding( textures.Add(new TranslatedImageBinding(
texture, texture,
Gen5ShaderTranslator.RequiresStorageImage( Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode),
binding,
exportEvaluation.ImageBindings),
binding.MipLevel ?? 0, binding.MipLevel ?? 0,
binding.SamplerDescriptor)); binding.SamplerDescriptor));
} }
@@ -6193,7 +6030,7 @@ public static partial class AgcExports
attributeCount, attributeCount,
psInputEna, psInputEna,
psInputAddr, psInputAddr,
_storageBufferOffsetAlignment); VulkanVideoPresenter.GuestStorageBufferOffsetAlignment);
var guestGlobalBuffers = var guestGlobalBuffers =
pixelEvaluation.GlobalMemoryBindings.Count + pixelEvaluation.GlobalMemoryBindings.Count +
@@ -6229,7 +6066,7 @@ public static partial class AgcExports
pixelInputEnable: psInputEna, pixelInputEnable: psInputEna,
pixelInputAddress: psInputAddr, pixelInputAddress: psInputAddr,
storageBufferOffsetAlignment: storageBufferOffsetAlignment:
_storageBufferOffsetAlignment) || VulkanVideoPresenter.GuestStorageBufferOffsetAlignment) ||
!GuestGpu.Current.TryCompileVertexShader( !GuestGpu.Current.TryCompileVertexShader(
exportState, exportState,
exportEvaluation, exportEvaluation,
@@ -6241,7 +6078,7 @@ public static partial class AgcExports
scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers + 1, scalarRegisterBufferIndex: _bakeScalars ? -1 : guestGlobalBuffers + 1,
requiredVertexOutputCount: (int)GetInterpolatedAttributeCount(pixelState), requiredVertexOutputCount: (int)GetInterpolatedAttributeCount(pixelState),
storageBufferOffsetAlignment: storageBufferOffsetAlignment:
_storageBufferOffsetAlignment)) VulkanVideoPresenter.GuestStorageBufferOffsetAlignment))
{ {
ReturnPooledEvaluationArrays(exportEvaluation); ReturnPooledEvaluationArrays(exportEvaluation);
ReturnPooledEvaluationArrays(pixelEvaluation); ReturnPooledEvaluationArrays(pixelEvaluation);
@@ -6261,26 +6098,21 @@ public static partial class AgcExports
pixelStateFingerprint, pixelStateFingerprint,
compiled.Pixel, compiled.Pixel,
pixelState.Program); pixelState.Program);
GuestGpu.Current.CountShaderCompilation(); VulkanVideoPresenter.CountSpirvCompilation();
_graphicsShaderCache.TryAdd(shaderKey, compiled); _graphicsShaderCache.TryAdd(shaderKey, compiled);
} }
var imageBindings = pixelEvaluation.ImageBindings
.Concat(exportEvaluation.ImageBindings)
.ToArray();
var textures = new List<TranslatedImageBinding>( var textures = new List<TranslatedImageBinding>(
pixelEvaluation.ImageBindings.Count + pixelEvaluation.ImageBindings.Count +
exportEvaluation.ImageBindings.Count); exportEvaluation.ImageBindings.Count);
if (!TryAppendTranslatedImageBindings( if (!TryAppendTranslatedImageBindings(
pixelEvaluation.ImageBindings, pixelEvaluation.ImageBindings,
imageBindings,
textures, textures,
pixelShaderAddress, pixelShaderAddress,
exportShaderAddress, exportShaderAddress,
out error) || out error) ||
!TryAppendTranslatedImageBindings( !TryAppendTranslatedImageBindings(
exportEvaluation.ImageBindings, exportEvaluation.ImageBindings,
imageBindings,
textures, textures,
pixelShaderAddress, pixelShaderAddress,
exportShaderAddress, exportShaderAddress,
@@ -6359,7 +6191,6 @@ public static partial class AgcExports
private static bool TryAppendTranslatedImageBindings( private static bool TryAppendTranslatedImageBindings(
IReadOnlyList<Gen5ImageBinding> bindings, IReadOnlyList<Gen5ImageBinding> bindings,
IReadOnlyList<Gen5ImageBinding> stageBindings,
List<TranslatedImageBinding> textures, List<TranslatedImageBinding> textures,
ulong pixelShaderAddress, ulong pixelShaderAddress,
ulong exportShaderAddress, ulong exportShaderAddress,
@@ -6384,9 +6215,8 @@ public static partial class AgcExports
0, 1, 1, Gen5TextureFormatR8G8B8A8Unorm, 0, 0, 0, 0, 0, 1, 0xFAC); 0, 1, 1, Gen5TextureFormatR8G8B8A8Unorm, 0, 0, 0, 0, 0, 1, 0xFAC);
} }
var isStorage = Gen5ShaderTranslator.RequiresStorageImage( var isStorage =
binding, Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
stageBindings);
if (_traceAgcShader || _tracePixelShaderAddress == pixelShaderAddress) if (_traceAgcShader || _tracePixelShaderAddress == pixelShaderAddress)
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
@@ -6539,7 +6369,7 @@ public static partial class AgcExports
var bytesPerIndex = is32Bit ? sizeof(uint) : sizeof(ushort); var bytesPerIndex = is32Bit ? sizeof(uint) : sizeof(ushort);
var byteOffset = checked((ulong)state.DrawIndexOffset * (uint)bytesPerIndex); var byteOffset = checked((ulong)state.DrawIndexOffset * (uint)bytesPerIndex);
var byteCount = checked((int)(indexCount * (uint)bytesPerIndex)); var byteCount = checked((int)(indexCount * (uint)bytesPerIndex));
var data = GuestDataPool.Shared.Rent(byteCount); var data = VulkanVideoPresenter.GuestDataPool.Rent(byteCount);
var span = data.AsSpan(0, byteCount); var span = data.AsSpan(0, byteCount);
var address = state.IndexBufferAddress + byteOffset; var address = state.IndexBufferAddress + byteOffset;
if (ctx.Memory.TryRead(address, span) || if (ctx.Memory.TryRead(address, span) ||
@@ -6548,7 +6378,7 @@ public static partial class AgcExports
return new GuestIndexBuffer(data, byteCount, is32Bit, Pooled: true); return new GuestIndexBuffer(data, byteCount, is32Bit, Pooled: true);
} }
GuestDataPool.Shared.Return(data); VulkanVideoPresenter.GuestDataPool.Return(data);
return null; return null;
} }
@@ -6575,7 +6405,7 @@ public static partial class AgcExports
var byteOffset = checked((ulong)state.DrawIndexOffset * (uint)bytesPerIndex); var byteOffset = checked((ulong)state.DrawIndexOffset * (uint)bytesPerIndex);
var address = state.IndexBufferAddress + byteOffset; var address = state.IndexBufferAddress + byteOffset;
const int chunkBytes = 64 * 1024; const int chunkBytes = 64 * 1024;
var scratch = GuestDataPool.Shared.Rent(chunkBytes); var scratch = VulkanVideoPresenter.GuestDataPool.Rent(chunkBytes);
var remaining = drawCount; var remaining = drawCount;
var maxIndex = 0u; var maxIndex = 0u;
var sawIndex = false; var sawIndex = false;
@@ -6617,7 +6447,7 @@ public static partial class AgcExports
} }
finally finally
{ {
GuestDataPool.Shared.Return(scratch); VulkanVideoPresenter.GuestDataPool.Return(scratch);
} }
var indexedRecords = sawIndex && maxIndex != uint.MaxValue var indexedRecords = sawIndex && maxIndex != uint.MaxValue
@@ -6741,7 +6571,7 @@ public static partial class AgcExports
{ {
hash = (hash ^ ( hash = (hash ^ (
binding.BaseAddress & binding.BaseAddress &
(_storageBufferOffsetAlignment - 1))) * prime; (VulkanVideoPresenter.GuestStorageBufferOffsetAlignment - 1))) * prime;
} }
if (evaluation.ComputeSystemRegisters is { } computeSystemRegisters) if (evaluation.ComputeSystemRegisters is { } computeSystemRegisters)
@@ -6831,8 +6661,7 @@ public static partial class AgcExports
scissor, scissor,
DecodeViewport(registers, target.Width, target.Height, scissor), DecodeViewport(registers, target.Width, target.Height, scissor),
DecodeRasterState(registers), DecodeRasterState(registers),
DecodeDepthState(registers), DecodeDepthState(registers));
DecodeBlendConstant(registers));
} }
private static GuestRenderState CreateRenderState( private static GuestRenderState CreateRenderState(
@@ -6865,8 +6694,7 @@ public static partial class AgcExports
scissor, scissor,
DecodeViewport(registers, target.Width, target.Height, scissor), DecodeViewport(registers, target.Width, target.Height, scissor),
DecodeRasterState(registers), DecodeRasterState(registers),
DecodeDepthState(registers), DecodeDepthState(registers));
DecodeBlendConstant(registers));
} }
// DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE // DB_DEPTH_CONTROL (context register 0x200): Z_ENABLE bit1, Z_WRITE_ENABLE
@@ -6971,22 +6799,6 @@ public static partial class AgcExports
return new GuestRasterState(cullFront, cullBack, frontFaceClockwise, wireframe); return new GuestRasterState(cullFront, cullBack, frontFaceClockwise, wireframe);
} }
/// <summary>CB_BLEND_RED..ALPHA carry the constant blend color as raw
/// float bits; unwritten registers read as the reset value (0.0).</summary>
private static GuestBlendConstant DecodeBlendConstant(
IReadOnlyDictionary<uint, uint> registers)
{
registers.TryGetValue(CbBlendRed, out var red);
registers.TryGetValue(CbBlendGreen, out var green);
registers.TryGetValue(CbBlendBlue, out var blue);
registers.TryGetValue(CbBlendAlpha, out var alpha);
return new GuestBlendConstant(
BitConverter.Int32BitsToSingle(unchecked((int)red)),
BitConverter.Int32BitsToSingle(unchecked((int)green)),
BitConverter.Int32BitsToSingle(unchecked((int)blue)),
BitConverter.Int32BitsToSingle(unchecked((int)alpha)));
}
private static GuestBlendState DecodeBlendState( private static GuestBlendState DecodeBlendState(
IReadOnlyDictionary<uint, uint> registers, IReadOnlyDictionary<uint, uint> registers,
uint slot) uint slot)
@@ -7499,7 +7311,7 @@ public static partial class AgcExports
IReadOnlyList<uint> registers, IReadOnlyList<uint> registers,
IReadOnlyList<Gen5GlobalMemoryBinding> bindings) IReadOnlyList<Gen5GlobalMemoryBinding> bindings)
{ {
var bytes = GuestDataPool.Shared.Rent( var bytes = VulkanVideoPresenter.GuestDataPool.Rent(
GetRuntimeScalarBufferLength(bindings.Count)); GetRuntimeScalarBufferLength(bindings.Count));
PackRuntimeScalarStateInto(bytes, registers, bindings); PackRuntimeScalarStateInto(bytes, registers, bindings);
return bytes; return bytes;
@@ -7525,7 +7337,7 @@ public static partial class AgcExports
{ {
var byteBias = checked((uint)( var byteBias = checked((uint)(
bindings[index].BaseAddress & bindings[index].BaseAddress &
(_storageBufferOffsetAlignment - 1))); (VulkanVideoPresenter.GuestStorageBufferOffsetAlignment - 1)));
BinaryPrimitives.WriteUInt32LittleEndian( BinaryPrimitives.WriteUInt32LittleEndian(
bytes.AsSpan(biasOffset + index * sizeof(uint), sizeof(uint)), bytes.AsSpan(biasOffset + index * sizeof(uint), sizeof(uint)),
byteBias); byteBias);
@@ -7562,13 +7374,11 @@ public static partial class AgcExports
/// </summary> /// </summary>
private static void ReturnPooledEvaluationArrays(Gen5ShaderEvaluation evaluation) private static void ReturnPooledEvaluationArrays(Gen5ShaderEvaluation evaluation)
{ {
var returned = new HashSet<byte[]>(
System.Collections.Generic.ReferenceEqualityComparer.Instance);
foreach (var binding in evaluation.GlobalMemoryBindings) foreach (var binding in evaluation.GlobalMemoryBindings)
{ {
if (binding.DataPooled && returned.Add(binding.Data)) if (binding.DataPooled)
{ {
GuestDataPool.Shared.Return(binding.Data); VulkanVideoPresenter.GuestDataPool.Return(binding.Data);
} }
} }
@@ -7576,9 +7386,9 @@ public static partial class AgcExports
{ {
foreach (var binding in vertexInputs) foreach (var binding in vertexInputs)
{ {
if (binding.DataPooled && returned.Add(binding.Data)) if (binding.DataPooled)
{ {
GuestDataPool.Shared.Return(binding.Data); VulkanVideoPresenter.GuestDataPool.Return(binding.Data);
} }
} }
} }
@@ -7596,15 +7406,13 @@ public static partial class AgcExports
bool vertex, bool vertex,
bool index) bool index)
{ {
var returned = new HashSet<byte[]>(
System.Collections.Generic.ReferenceEqualityComparer.Instance);
if (globals) if (globals)
{ {
foreach (var binding in draw.GlobalMemoryBindings) foreach (var binding in draw.GlobalMemoryBindings)
{ {
if (binding.DataPooled && returned.Add(binding.Data)) if (binding.DataPooled)
{ {
GuestDataPool.Shared.Return(binding.Data); VulkanVideoPresenter.GuestDataPool.Return(binding.Data);
} }
} }
} }
@@ -7613,17 +7421,16 @@ public static partial class AgcExports
{ {
foreach (var binding in draw.VertexInputs) foreach (var binding in draw.VertexInputs)
{ {
if (binding.DataPooled && returned.Add(binding.Data)) if (binding.DataPooled)
{ {
GuestDataPool.Shared.Return(binding.Data); VulkanVideoPresenter.GuestDataPool.Return(binding.Data);
} }
} }
} }
if (index && draw.IndexBuffer is { Pooled: true } indexBuffer && if (index && draw.IndexBuffer is { Pooled: true } indexBuffer)
returned.Add(indexBuffer.Data))
{ {
GuestDataPool.Shared.Return(indexBuffer.Data); VulkanVideoPresenter.GuestDataPool.Return(indexBuffer.Data);
} }
} }
@@ -7880,7 +7687,7 @@ public static partial class AgcExports
if (!isStorage && if (!isStorage &&
descriptor.Address != 0 && descriptor.Address != 0 &&
GuestGpu.Current.IsGpuGuestImageAvailable( VulkanVideoPresenter.IsGuestImageAvailable(
descriptor.Address, descriptor.Address,
descriptor.Format, descriptor.Format,
descriptor.NumberType)) descriptor.NumberType))
@@ -7909,7 +7716,7 @@ public static partial class AgcExports
{ {
var initialPixels = Array.Empty<byte>(); var initialPixels = Array.Empty<byte>();
var uploadKnown = descriptor.Address != 0 && var uploadKnown = descriptor.Address != 0 &&
GuestGpu.Current.IsGuestImageUploadKnown( VulkanVideoPresenter.IsGuestImageUploadKnown(
descriptor.Address, descriptor.Address,
descriptor.Format, descriptor.Format,
descriptor.NumberType); descriptor.NumberType);
@@ -7990,8 +7797,8 @@ public static partial class AgcExports
if (!_textureCopySkipDisabled && if (!_textureCopySkipDisabled &&
descriptor.Address != 0 && descriptor.Address != 0 &&
!SharpEmu.HLE.GuestImageWriteTracker.PeekDirty(descriptor.Address) && !SharpEmu.HLE.GuestImageWriteTracker.PeekDirty(descriptor.Address) &&
GuestGpu.Current.IsTextureContentCached( VulkanVideoPresenter.IsTextureContentCached(
new TextureContentIdentity( new VulkanVideoPresenter.TextureContentIdentity(
descriptor.Address, descriptor.Address,
descriptor.Width, descriptor.Width,
descriptor.Height, descriptor.Height,
@@ -8094,15 +7901,12 @@ public static partial class AgcExports
CpuContext ctx, CpuContext ctx,
RenderTargetDescriptor target) RenderTargetDescriptor target)
{ {
if (!GuestGpu.Current.GuestImageWantsInitialData(target.Address)) if (!VulkanVideoPresenter.GuestImageWantsInitialData(target.Address))
{ {
return; return;
} }
var byteCount = VulkanVideoPresenter.GetGuestImageByteCount( var byteCount = (ulong)target.Width * target.Height * 4;
target.Format,
target.Width,
target.Height);
if (byteCount == 0 || byteCount > MaxPresentedTextureBytes) if (byteCount == 0 || byteCount > MaxPresentedTextureBytes)
{ {
return; return;
@@ -8120,7 +7924,7 @@ public static partial class AgcExports
if (nonZero) if (nonZero)
{ {
GuestGpu.Current.ProvideGuestImageInitialData(target.Address, initialData); VulkanVideoPresenter.ProvideGuestImageInitialData(target.Address, initialData);
} }
} }
@@ -8417,14 +8221,9 @@ public static partial class AgcExports
ulong packetAddress, ulong packetAddress,
uint packetLength, uint packetLength,
uint opcode, uint opcode,
out ComputeDispatch dispatch, out ComputeDispatch dispatch)
out ulong indirectDimsRetryAddress)
{ {
dispatch = default; dispatch = default;
// Non-zero only when this is an INDIRECT dispatch whose dimensions read as
// zero — meaning the producing GPU dispatch that computes them has not run
// yet. The caller suspends on this address instead of dropping the work.
indirectDimsRetryAddress = 0;
ulong dimensionsAddress; ulong dimensionsAddress;
uint initiator; uint initiator;
string dispatchSource; string dispatchSource;
@@ -8473,17 +8272,6 @@ public static partial class AgcExports
if (dispatchEndX == 0 || dispatchEndY == 0 || dispatchEndZ == 0) if (dispatchEndX == 0 || dispatchEndY == 0 || dispatchEndZ == 0)
{ {
// Indirect dispatches read their dimensions from a guest buffer a
// prior GPU dispatch fills. Zero here means that producer has not run
// yet — signal the caller to suspend on the dims buffer and retry,
// rather than dropping the work (which black-screens GPU-driven games
// like Astro Bot). Direct dispatches carry dims inline, so a zero is
// genuinely malformed and still rejected.
if (opcode == ItDispatchIndirect)
{
indirectDimsRetryAddress = dimensionsAddress;
}
return RejectComputeDispatch( return RejectComputeDispatch(
dimensionsAddress, dimensionsAddress,
initiator, initiator,
@@ -8734,8 +8522,7 @@ public static partial class AgcExports
var hasStorageBinding = false; var hasStorageBinding = false;
foreach (var binding in bindings) foreach (var binding in bindings)
{ {
var isStorage = Gen5ShaderTranslator.RequiresStorageImage(binding, bindings); var isStorage = Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
var writesStorage = Gen5ShaderTranslator.IsStorageImageOperation(binding.Opcode);
var descriptorValid = TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture); var descriptorValid = TryDecodeTextureDescriptor(binding.ResourceDescriptor, out var texture);
if (!descriptorValid) if (!descriptorValid)
{ {
@@ -8756,7 +8543,7 @@ public static partial class AgcExports
$"0x{texture.Address:X16}:{texture.Width}x{texture.Height}:" + $"0x{texture.Address:X16}:{texture.Width}x{texture.Height}:" +
$"fmt{texture.Format}/num{texture.NumberType}/tile{texture.TileMode}" + $"fmt{texture.Format}/num{texture.NumberType}/tile{texture.TileMode}" +
$"{descriptorState}/{ProbeTexture(ctx, texture)}"); $"{descriptorState}/{ProbeTexture(ctx, texture)}");
if (writesStorage && descriptorValid && texture.Address != 0) if (isStorage && descriptorValid && texture.Address != 0)
{ {
gpuState.ComputeImageWriters[texture.Address] = new ComputeImageWriter( gpuState.ComputeImageWriters[texture.Address] = new ComputeImageWriter(
sequence, sequence,
@@ -8818,7 +8605,7 @@ public static partial class AgcExports
// still queued, so the clear could erase newly constructed CPU // still queued, so the clear could erase newly constructed CPU
// objects. Waiting on the work sequence also retires preceding // objects. Waiting on the work sequence also retires preceding
// Vulkan writes before the next evaluator snapshot is captured. // Vulkan writes before the next evaluator snapshot is captured.
if (!GuestGpu.Current.WaitForGuestWork(semanticCopySequence)) if (!VulkanVideoPresenter.WaitForGuestWork(semanticCopySequence))
{ {
computeError = computeError =
$"semantic-global-write-sync-timeout sequence={semanticCopySequence}"; $"semantic-global-write-sync-timeout sequence={semanticCopySequence}";
@@ -8836,7 +8623,7 @@ public static partial class AgcExports
localSizeY, localSizeY,
localSizeZ, localSizeZ,
dispatch.WaveLaneCount, dispatch.WaveLaneCount,
_storageBufferOffsetAlignment); VulkanVideoPresenter.GuestStorageBufferOffsetAlignment);
var guestGlobalBufferCount = evaluation.GlobalMemoryBindings.Count; var guestGlobalBufferCount = evaluation.GlobalMemoryBindings.Count;
var totalGlobalBufferCount = _bakeScalars var totalGlobalBufferCount = _bakeScalars
? guestGlobalBufferCount ? guestGlobalBufferCount
@@ -8858,7 +8645,7 @@ public static partial class AgcExports
: guestGlobalBufferCount, : guestGlobalBufferCount,
waveLaneCount: dispatch.WaveLaneCount, waveLaneCount: dispatch.WaveLaneCount,
storageBufferOffsetAlignment: storageBufferOffsetAlignment:
_storageBufferOffsetAlignment)) VulkanVideoPresenter.GuestStorageBufferOffsetAlignment))
{ {
DumpCompiledShader( DumpCompiledShader(
"cs", "cs",
@@ -8878,7 +8665,7 @@ public static partial class AgcExports
out _); out _);
var globalMemoryBuffers = var globalMemoryBuffers =
CreateTranslatedComputeGlobalBuffers(evaluation); CreateTranslatedComputeGlobalBuffers(evaluation);
GuestGpu.Current.SubmitComputeDispatch( var workSequence = GuestGpu.Current.SubmitComputeDispatch(
shaderAddress, shaderAddress,
computeShader, computeShader,
textures, textures,
@@ -8897,9 +8684,12 @@ public static partial class AgcExports
dispatch.ThreadCountX, dispatch.ThreadCountX,
dispatch.ThreadCountY, dispatch.ThreadCountY,
dispatch.ThreadCountZ); dispatch.ThreadCountZ);
// Vulkan queue order keeps dependent dispatches coherent. CPU visibility is
// published by explicit PM4 release/write actions instead of per dispatch.
gpuDispatch = true; gpuDispatch = true;
if (writesGlobalMemory &&
!VulkanVideoPresenter.WaitForGuestWork(workSequence))
{
computeError = $"global-write-sync-timeout sequence={workSequence}";
}
} }
} }
@@ -9090,7 +8880,7 @@ public static partial class AgcExports
} }
var destinationAddress = destination.BaseAddress; var destinationAddress = destination.BaseAddress;
workSequence = GuestGpu.Current.SubmitOrderedGuestAction( workSequence = VulkanVideoPresenter.SubmitOrderedGuestAction(
() => () =>
{ {
if (!ctx.Memory.TryWrite(destinationAddress, output)) if (!ctx.Memory.TryWrite(destinationAddress, output))
@@ -9104,7 +8894,7 @@ public static partial class AgcExports
GuestImageWriteTracker.Track( GuestImageWriteTracker.Track(
destinationAddress, destinationAddress,
(ulong)output.Length, (ulong)output.Length,
GuestGpu.Current.CurrentGuestWorkSequenceForDiagnostics, VulkanVideoPresenter.CurrentGuestWorkSequenceForDiagnostics,
"agc.masked-dword-copy"); "agc.masked-dword-copy");
}, },
$"masked_dword_copy dst=0x{destinationAddress:X16} bytes={output.Length}"); $"masked_dword_copy dst=0x{destinationAddress:X16} bytes={output.Length}");
@@ -9793,7 +9583,7 @@ public static partial class AgcExports
pixelInputEnable: psInputEna, pixelInputEnable: psInputEna,
pixelInputAddress: psInputAddr, pixelInputAddress: psInputAddr,
storageBufferOffsetAlignment: storageBufferOffsetAlignment:
_storageBufferOffsetAlignment)) VulkanVideoPresenter.GuestStorageBufferOffsetAlignment))
{ {
TraceAgcShader( TraceAgcShader(
$"agc.shader_spirv ps=0x{pixelShaderAddress:X16} " + $"agc.shader_spirv ps=0x{pixelShaderAddress:X16} " +
@@ -11417,35 +11207,4 @@ public static partial class AgcExports
TraceAgc($"agc.driver_unregister_resource handle={resourceHandle}"); TraceAgc($"agc.driver_unregister_resource handle={resourceHandle}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK); return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
} }
// Tessellation-factor ring and hull-shader off-chip buffers are guest-driver
// configuration for on-hardware tessellation memory. Our translator handles
// shader execution directly, so there is no guest-side ring to program: the
// guest driver only needs these to report success so init proceeds. Games
// (e.g. Unity titles) call them during GPU setup and stall if unresolved.
[SysAbiExport(
Nid = "XlNp7jzGiPo",
ExportName = "sceAgcDriverSetTFRing",
Target = Generation.Gen5,
LibraryName = "libSceAgcDriver")]
public static int DriverSetTFRing(CpuContext ctx)
{
TraceAgc(
$"agc.driver_set_tf_ring ring=0x{ctx[CpuRegister.Rdi]:X16} " +
$"size=0x{(uint)ctx[CpuRegister.Rsi]:X8}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
[SysAbiExport(
Nid = "MM4IZSEYytQ",
ExportName = "sceAgcDriverSetHsOffchipParam",
Target = Generation.Gen5,
LibraryName = "libSceAgcDriver")]
public static int DriverSetHsOffchipParam(CpuContext ctx)
{
TraceAgc(
$"agc.driver_set_hs_offchip_param buffer=0x{ctx[CpuRegister.Rdi]:X16} " +
$"param=0x{(uint)ctx[CpuRegister.Rsi]:X8}");
return ctx.SetReturn(OrbisGen2Result.ORBIS_GEN2_OK);
}
} }
@@ -3,7 +3,6 @@
using System.Diagnostics.CodeAnalysis; using System.Diagnostics.CodeAnalysis;
using System.Runtime.CompilerServices; using System.Runtime.CompilerServices;
using SharpEmu.Libs.Gpu;
using SharpEmu.Libs.Kernel; using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.VideoOut; using SharpEmu.Libs.VideoOut;
using SharpEmu.ShaderCompiler; using SharpEmu.ShaderCompiler;
@@ -27,8 +26,8 @@ internal static class AgcShaderCompilerHooks
internal static void Install() internal static void Install()
{ {
Gen5ShaderScalarEvaluator.FallbackMemoryReader = Gen5ShaderScalarEvaluator.FallbackMemoryReader =
KernelMemoryCompatExports.TryReadShaderGuestMemory; KernelMemoryCompatExports.TryReadTrackedLibcHeap;
Gen5ShaderScalarEvaluator.GlobalMemoryPool = Gen5ShaderScalarEvaluator.GlobalMemoryPool =
GuestDataPool.Shared; VulkanVideoPresenter.GuestDataPool;
} }
} }
+2 -181
View File
@@ -37,26 +37,10 @@ internal static class GpuWaitRegistry
public long RegisteredTicks; public long RegisteredTicks;
public bool StaleReported; public bool StaleReported;
public object? State; public object? State;
// Latched by LatchSatisfiedByValue when a producer wrote a value that
// satisfies this waiter. The label is frequently reused (reset to 0 for
// the next frame) immediately after the producing write, so re-reading
// guest memory at wake time can miss the transient satisfied window.
// Latching records satisfaction at the moment of the write instead.
public bool Latched;
// Non-zero for indirect-dispatch dimension retries: a bounded deadline
// (Stopwatch ticks) after which the waiter is resumed even if unsatisfied,
// so a legitimately empty indirect dispatch can never stall forever.
public long RetryDeadlineTicks;
} }
private static readonly object _gate = new(); private static readonly object _gate = new();
private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new(); private static readonly Dictionary<ulong, List<WaitingDcb>> _waiters = new();
// The last value each label producer wrote. Used only by the deadlock
// breaker: our serial submission parser cannot model two GPU queues running
// concurrently, so a label written -> reset -> re-waited across queues can
// cycle forever even though a real producer did signal it. Keyed by (memory,
// address) so distinct guest processes never alias.
private static readonly Dictionary<(object, ulong), ulong> _lastProduced = new();
public static int Count public static int Count
{ {
@@ -130,14 +114,8 @@ internal static class GpuWaitRegistry
continue; continue;
} }
var satisfied = list[i].Latched; var value = readValue(address, list[i].Is64Bit);
if (!satisfied) if (value is null || !Compare(list[i], value.Value))
{
var value = readValue(address, list[i].Is64Bit);
satisfied = value is not null && Compare(list[i], value.Value);
}
if (!satisfied)
{ {
continue; continue;
} }
@@ -258,162 +236,6 @@ internal static class GpuWaitRegistry
return matches; return matches;
} }
/// <summary>
/// Records satisfaction for every waiter at <paramref name="address"/> whose
/// condition is met by <paramref name="value"/> — the value a producer just
/// wrote to that label. Called from the ordered producer side effect so a
/// same-frame label reset cannot lose the wakeup. The waiters stay registered
/// (latched) and are drained by the next CollectSatisfied. Returns true when
/// at least one waiter latched, so the caller can trigger a wake pass.
/// </summary>
public static bool LatchSatisfiedByValue(object memory, ulong address, ulong value)
{
var latchedAny = false;
lock (_gate)
{
if (!_waiters.TryGetValue(address, out var list))
{
return false;
}
for (var i = 0; i < list.Count; i++)
{
var waiter = list[i];
if (waiter.Latched ||
!ReferenceEquals(waiter.Memory, memory) ||
!Compare(waiter, value))
{
continue;
}
waiter.Latched = true;
list[i] = waiter;
latchedAny = true;
}
}
return latchedAny;
}
/// <summary>
/// Removes and returns waiters carrying a <see cref="WaitingDcb.RetryDeadlineTicks"/>
/// that has elapsed. Used for indirect-dispatch dimension retries: the caller
/// resumes them so a genuinely empty dispatch (dims that never become non-zero)
/// is dropped after a bounded wait instead of stalling the queue forever.
/// </summary>
public static List<WaitingDcb>? CollectExpiredRetries(object memory, long nowTicks)
{
List<WaitingDcb>? expired = null;
lock (_gate)
{
List<ulong>? emptied = null;
foreach (var (address, list) in _waiters)
{
for (var i = list.Count - 1; i >= 0; i--)
{
var waiter = list[i];
if (waiter.RetryDeadlineTicks == 0 ||
!ReferenceEquals(waiter.Memory, memory) ||
nowTicks < waiter.RetryDeadlineTicks)
{
continue;
}
expired ??= new List<WaitingDcb>();
expired.Add(waiter);
list.RemoveAt(i);
}
if (list.Count == 0)
{
emptied ??= new List<ulong>();
emptied.Add(address);
}
}
if (emptied is not null)
{
foreach (var address in emptied)
{
_waiters.Remove(address);
}
}
}
return expired;
}
/// <summary>Records the value a label producer wrote, for the deadlock
/// breaker. Also latches any already-waiting waiter it satisfies.</summary>
public static bool RecordProduced(object memory, ulong address, ulong value)
{
lock (_gate)
{
if (_lastProduced.Count >= 8192)
{
_lastProduced.Clear();
}
_lastProduced[(memory, address)] = value;
}
return LatchSatisfiedByValue(memory, address, value);
}
/// <summary>
/// Breaks cross-queue GPU deadlocks the serial parser cannot avoid: returns
/// (and removes) waiters that have been stuck longer than
/// <paramref name="minAgeTicks"/> and whose condition is satisfied by the
/// last value a real producer wrote to their label — even though guest
/// memory has since been reset. Never fabricates a value: a waiter is only
/// released when an actual producer signalled it at least once.
/// </summary>
public static List<WaitingDcb>? CollectDeadlockBroken(
object memory,
long nowTicks,
long minAgeTicks)
{
List<WaitingDcb>? broken = null;
lock (_gate)
{
List<ulong>? emptied = null;
foreach (var (address, list) in _waiters)
{
for (var i = list.Count - 1; i >= 0; i--)
{
var waiter = list[i];
if (!ReferenceEquals(waiter.Memory, memory) ||
nowTicks - waiter.RegisteredTicks < minAgeTicks ||
!_lastProduced.TryGetValue((memory, address), out var produced) ||
!Compare(waiter, produced))
{
continue;
}
broken ??= new List<WaitingDcb>();
broken.Add(waiter);
list.RemoveAt(i);
}
if (list.Count == 0)
{
emptied ??= new List<ulong>();
emptied.Add(address);
}
}
if (emptied is not null)
{
foreach (var address in emptied)
{
_waiters.Remove(address);
}
}
}
return broken;
}
public static bool Compare(in WaitingDcb waiter, ulong value) public static bool Compare(in WaitingDcb waiter, ulong value)
{ {
var masked = value & waiter.Mask; var masked = value & waiter.Mask;
@@ -438,7 +260,6 @@ internal static class GpuWaitRegistry
lock (_gate) lock (_gate)
{ {
_waiters.Clear(); _waiters.Clear();
_lastProduced.Clear();
} }
} }
} }
+17 -10
View File
@@ -6,7 +6,6 @@ using SharpEmu.Libs.Kernel;
using System.Buffers; using System.Buffers;
using System.Buffers.Binary; using System.Buffers.Binary;
using System.Collections.Concurrent; using System.Collections.Concurrent;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Libs.Ampr; namespace SharpEmu.Libs.Ampr;
@@ -44,17 +43,17 @@ public static class AmprExports
{ {
public CachedHostFile(string path) public CachedHostFile(string path)
{ {
Handle = File.OpenHandle( Stream = new FileStream(
path, path,
FileMode.Open, FileMode.Open,
FileAccess.Read, FileAccess.Read,
FileShare.ReadWrite | FileShare.Delete, FileShare.ReadWrite | FileShare.Delete,
bufferSize: 1024 * 1024,
FileOptions.RandomAccess); FileOptions.RandomAccess);
Length = RandomAccess.GetLength(Handle);
} }
public SafeFileHandle Handle { get; } public object Gate { get; } = new();
public long Length { get; } public FileStream Stream { get; }
} }
[SysAbiExport( [SysAbiExport(
@@ -736,7 +735,13 @@ public static class AmprExports
return openResult; return openResult;
} }
if (fileOffset >= (ulong)cachedFile.Length) long fileLength;
lock (cachedFile.Gate)
{
fileLength = cachedFile.Stream.Length;
}
if (fileOffset >= (ulong)fileLength)
{ {
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
@@ -755,10 +760,12 @@ public static class AmprExports
} }
var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead); var request = (int)Math.Min((ulong)buffer.Length, size - bytesRead);
var read = RandomAccess.Read( int read;
cachedFile.Handle, lock (cachedFile.Gate)
buffer.AsSpan(0, request), {
unchecked((long)absoluteOffset)); cachedFile.Stream.Position = unchecked((long)absoluteOffset);
read = cachedFile.Stream.Read(buffer, 0, request);
}
if (read <= 0) if (read <= 0)
{ {
@@ -121,33 +121,6 @@ public static class AppContentExports
return (int)OrbisGen2Result.ORBIS_GEN2_OK; return (int)OrbisGen2Result.ORBIS_GEN2_OK;
} }
// Download data is not emulated as a real quota; report a comfortable
// fixed amount of free space so titles never take the "storage full" path.
[SysAbiExport(
Nid = "Gl6w5i0JokY",
ExportName = "sceAppContentDownloadDataGetAvailableSpaceKb",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAppContent")]
public static int AppContentDownloadDataGetAvailableSpaceKb(CpuContext ctx)
{
const ulong availableSpaceKb = 1024UL * 1024UL; // 1 GiB
var availableSpaceAddress = ctx[CpuRegister.Rsi];
if (availableSpaceAddress == 0)
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT;
}
Span<byte> spaceBytes = stackalloc byte[sizeof(ulong)];
BinaryPrimitives.WriteUInt64LittleEndian(spaceBytes, availableSpaceKb);
if (!ctx.Memory.TryWrite(availableSpaceAddress, spaceBytes))
{
return (int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT;
}
ctx[CpuRegister.Rax] = 0;
return (int)OrbisGen2Result.ORBIS_GEN2_OK;
}
private static bool TryReadUserDefinedParam(uint paramId, out int value) private static bool TryReadUserDefinedParam(uint paramId, out int value)
{ {
value = 0; value = 0;
+4 -143
View File
@@ -10,29 +10,8 @@ namespace SharpEmu.Libs.Audio;
public static class AjmExports public static class AjmExports
{ {
private const int OrbisAjmErrorInvalidContext = unchecked((int)0x80930002); private static readonly ConcurrentDictionary<uint, byte> Contexts = new();
private const int OrbisAjmErrorInvalidInstance = unchecked((int)0x80930003);
private const int OrbisAjmErrorInvalidParameter = unchecked((int)0x80930005);
private const int OrbisAjmErrorOutOfResources = unchecked((int)0x80930007);
private const int OrbisAjmErrorCodecAlreadyRegistered = unchecked((int)0x80930009);
private const int OrbisAjmErrorCodecNotRegistered = unchecked((int)0x8093000A);
private const int OrbisAjmErrorWrongRevisionFlag = unchecked((int)0x8093000B);
private const uint MaxCodecType = 23;
private const int MaxInstanceIndex = 0x2FFF;
private static readonly ConcurrentDictionary<uint, AjmContextState> Contexts = new();
private static int _nextContextId; private static int _nextContextId;
private sealed class AjmContextState
{
public object Gate { get; } = new();
public HashSet<uint> RegisteredCodecs { get; } = new();
public Dictionary<uint, uint> InstancesBySlot { get; } = new();
public int NextInstanceIndex { get; set; }
}
public static int AjmInitialize(CpuContext ctx) public static int AjmInitialize(CpuContext ctx)
{ {
var reserved = ctx[CpuRegister.Rdi]; var reserved = ctx[CpuRegister.Rdi];
@@ -50,7 +29,7 @@ public static class AjmExports
return unchecked((int)0x806A0001); return unchecked((int)0x806A0001);
} }
Contexts[contextId] = new AjmContextState(); Contexts[contextId] = 0;
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal)) if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
{ {
Console.Error.WriteLine( Console.Error.WriteLine(
@@ -83,22 +62,9 @@ public static class AjmExports
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]); var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
var codecType = unchecked((uint)ctx[CpuRegister.Rsi]); var codecType = unchecked((uint)ctx[CpuRegister.Rsi]);
var reserved = ctx[CpuRegister.Rdx]; var reserved = ctx[CpuRegister.Rdx];
if (codecType >= MaxCodecType || reserved != 0) if (reserved != 0 || !Contexts.ContainsKey(contextId))
{ {
return ctx.SetReturn(OrbisAjmErrorInvalidParameter); return unchecked((int)0x806A0001);
}
if (!Contexts.TryGetValue(contextId, out var state))
{
return ctx.SetReturn(OrbisAjmErrorInvalidContext);
}
lock (state.Gate)
{
if (!state.RegisteredCodecs.Add(codecType))
{
return ctx.SetReturn(OrbisAjmErrorCodecAlreadyRegistered);
}
} }
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal)) if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
@@ -111,97 +77,6 @@ public static class AjmExports
return 0; return 0;
} }
[SysAbiExport(
Nid = "AxoDrINp4J8",
ExportName = "sceAjmInstanceCreate",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int AjmInstanceCreate(CpuContext ctx)
{
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
var codecType = unchecked((uint)ctx[CpuRegister.Rsi]);
var flags = ctx[CpuRegister.Rdx];
var outputAddress = ctx[CpuRegister.Rcx];
if (!Contexts.TryGetValue(contextId, out var state))
{
return ctx.SetReturn(OrbisAjmErrorInvalidContext);
}
if (codecType >= MaxCodecType || outputAddress == 0)
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
if ((flags & 0x7) == 0)
{
return ctx.SetReturn(OrbisAjmErrorWrongRevisionFlag);
}
uint instanceId;
lock (state.Gate)
{
if (!state.RegisteredCodecs.Contains(codecType))
{
return ctx.SetReturn(OrbisAjmErrorCodecNotRegistered);
}
if (state.InstancesBySlot.Count >= MaxInstanceIndex)
{
return ctx.SetReturn(OrbisAjmErrorOutOfResources);
}
var nextInstanceIndex = state.NextInstanceIndex;
uint instanceSlot;
do
{
nextInstanceIndex = nextInstanceIndex % MaxInstanceIndex + 1;
instanceSlot = unchecked((uint)nextInstanceIndex);
}
while (state.InstancesBySlot.ContainsKey(instanceSlot));
instanceId = (codecType << 14) | instanceSlot;
Span<byte> value = stackalloc byte[sizeof(uint)];
BinaryPrimitives.WriteUInt32LittleEndian(value, instanceId);
if (!ctx.Memory.TryWrite(outputAddress, value))
{
return ctx.SetReturn(OrbisAjmErrorInvalidParameter);
}
state.NextInstanceIndex = nextInstanceIndex;
state.InstancesBySlot.Add(instanceSlot, instanceId);
}
Trace($"instance_create context={contextId} codec={codecType} flags=0x{flags:X} instance=0x{instanceId:X8}");
return ctx.SetReturn(0);
}
[SysAbiExport(
Nid = "RbLbuKv8zho",
ExportName = "sceAjmInstanceDestroy",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAjm")]
public static int AjmInstanceDestroy(CpuContext ctx)
{
var contextId = unchecked((uint)ctx[CpuRegister.Rdi]);
var instanceId = unchecked((uint)ctx[CpuRegister.Rsi]);
if (!Contexts.TryGetValue(contextId, out var state))
{
return ctx.SetReturn(OrbisAjmErrorInvalidContext);
}
var instanceSlot = instanceId & 0x3FFF;
lock (state.Gate)
{
if (instanceSlot == 0 || !state.InstancesBySlot.Remove(instanceSlot))
{
return ctx.SetReturn(OrbisAjmErrorInvalidInstance);
}
}
Trace($"instance_destroy context={contextId} instance=0x{instanceId:X8}");
return ctx.SetReturn(0);
}
[SysAbiExport( [SysAbiExport(
Nid = "Wi7DtlLV+KI", Nid = "Wi7DtlLV+KI",
ExportName = "sceAjmModuleUnregister", ExportName = "sceAjmModuleUnregister",
@@ -226,18 +101,4 @@ public static class AjmExports
ctx[CpuRegister.Rax] = 0; ctx[CpuRegister.Rax] = 0;
return 0; return 0;
} }
internal static void ResetForTests()
{
Contexts.Clear();
Interlocked.Exchange(ref _nextContextId, 0);
}
private static void Trace(string message)
{
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_AJM"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine($"[LOADER][TRACE] ajm.{message}");
}
}
} }
+1 -88
View File
@@ -14,12 +14,6 @@ public static class AudioOutExports
private static readonly ConcurrentDictionary<int, PortState> Ports = new(); private static readonly ConcurrentDictionary<int, PortState> Ports = new();
private static int _nextPortHandle; private static int _nextPortHandle;
// Diagnostic: confirm sceAudioOutOutput is actually called and whether the
// guest submits real samples or silence. Gated so it costs nothing when off.
private static readonly bool _traceOutput = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_LOG_AUDIO_OUT"), "1", StringComparison.Ordinal);
private static long _outputCount;
private sealed class PortState : IDisposable private sealed class PortState : IDisposable
{ {
private readonly object _paceGate = new(); private readonly object _paceGate = new();
@@ -161,37 +155,6 @@ public static class AudioOutExports
return ctx.SetReturn(0); return ctx.SetReturn(0);
} }
[SysAbiExport(
Nid = "GrQ9s4IrNaQ",
ExportName = "sceAudioOutGetPortState",
Target = Generation.Gen4 | Generation.Gen5,
LibraryName = "libSceAudioOut")]
public static int AudioOutGetPortState(CpuContext ctx)
{
var handle = unchecked((int)ctx[CpuRegister.Rdi]);
var stateAddress = ctx[CpuRegister.Rsi];
if (stateAddress == 0 || !Ports.TryGetValue(handle, out var port))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
}
// SceAudioOutPortState: report a connected primary output at full volume
// so pacing/mixing code sees a live port. We do no host rerouting, so
// rerouteCounter and flag stay zero.
Span<byte> state = stackalloc byte[16];
state.Clear();
System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(state, 1);
System.Buffers.Binary.BinaryPrimitives.WriteUInt16LittleEndian(
state[2..], (ushort)port.Channels);
state[7] = 127;
if (!ctx.Memory.TryWrite(stateAddress, state))
{
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
}
return ctx.SetReturn(0);
}
[SysAbiExport( [SysAbiExport(
Nid = "QOQtbeDqsT4", Nid = "QOQtbeDqsT4",
ExportName = "sceAudioOutOutput", ExportName = "sceAudioOutOutput",
@@ -203,12 +166,7 @@ public static class AudioOutExports
var sourceAddress = ctx[CpuRegister.Rsi]; var sourceAddress = ctx[CpuRegister.Rsi];
if (!Ports.TryGetValue(handle, out var port)) if (!Ports.TryGetValue(handle, out var port))
{ {
// Host shutdown disposes the ports while guest audio threads are return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
// still draining their last buffers; report success so the guest
// winds down without a per-buffer error (and its WARN log flood).
return ctx.SetReturn(_shutdown
? 0
: (int)OrbisGen2Result.ORBIS_GEN2_ERROR_INVALID_ARGUMENT);
} }
if (sourceAddress == 0) if (sourceAddress == 0)
@@ -225,17 +183,6 @@ public static class AudioOutExports
return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT); return ctx.SetReturn((int)OrbisGen2Result.ORBIS_GEN2_ERROR_MEMORY_FAULT);
} }
if (_traceOutput)
{
var n = Interlocked.Increment(ref _outputCount);
if (n <= 8 || n % 200 == 0)
{
var peak = PeakAmplitude(source, port.IsFloat, port.BytesPerSample);
Console.Error.WriteLine(
$"[LOADER][TRACE] audioout.output#{n} handle={handle} bytes={source.Length} ch={port.Channels} float={port.IsFloat} vol={port.Volume:F2} peak={peak:F4} backend={(port.Backend is null ? "none" : "coreaudio")}");
}
}
if (port.Backend is null) if (port.Backend is null)
{ {
port.PaceSilence(); port.PaceSilence();
@@ -319,40 +266,8 @@ public static class AudioOutExports
return ctx.SetReturn(0); return ctx.SetReturn(0);
} }
// Peak normalized amplitude [0,1] of an interleaved PCM buffer, used only by
// the SHARPEMU_LOG_AUDIO_OUT diagnostic to distinguish real audio from silence.
private static float PeakAmplitude(ReadOnlySpan<byte> source, bool isFloat, int bytesPerSample)
{
var peak = 0f;
if (isFloat && bytesPerSample == 4)
{
for (var i = 0; i + 4 <= source.Length; i += 4)
{
var v = Math.Abs(System.Buffers.Binary.BinaryPrimitives.ReadSingleLittleEndian(source.Slice(i, 4)));
if (v > peak)
{
peak = v;
}
}
}
else if (bytesPerSample == 2)
{
for (var i = 0; i + 2 <= source.Length; i += 2)
{
var v = Math.Abs(System.Buffers.Binary.BinaryPrimitives.ReadInt16LittleEndian(source.Slice(i, 2)) / 32768f);
if (v > peak)
{
peak = v;
}
}
}
return peak;
}
public static void ShutdownAllPorts() public static void ShutdownAllPorts()
{ {
Volatile.Write(ref _shutdown, true);
foreach (var handle in Ports.Keys) foreach (var handle in Ports.Keys)
{ {
if (Ports.TryRemove(handle, out var port)) if (Ports.TryRemove(handle, out var port))
@@ -362,8 +277,6 @@ public static class AudioOutExports
} }
} }
private static bool _shutdown;
private static bool TryGetFormat( private static bool TryGetFormat(
int rawFormat, int rawFormat,
out int channels, out int channels,
@@ -1,154 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
namespace SharpEmu.Libs.Audio;
// PS5 acoustic-propagation (3D-audio ray/portal/room) module. We do not model
// acoustic propagation; the geometry-driven reverb/occlusion it produces is a
// quality feature, not a correctness gate. Games (e.g. Astro Bot) call it
// during audio init and hard-assert if any entry point is missing:
// ASSERT ... sceAudioPropagationSystemQueryMemory failed : 0x80020002
// The API is placement-style: QueryMemory reports a buffer size, the game
// allocates it, and the "system"/objects live inside that caller-owned buffer,
// so success-returning stubs let init proceed without us owning any state.
public static class AudioPropagationExports
{
private const int Ok = 0;
// QueryMemory reports the working-set size the caller must allocate before
// SystemCreate. rsi points at the out size/alignment; write a modest,
// aligned block so the caller's allocation succeeds.
[SysAbiExport(
Nid = "7xyAxrusLko",
ExportName = "sceAudioPropagationSystemQueryMemory",
Target = Generation.Gen5,
LibraryName = "libSceAudioPropagation")]
public static int SystemQueryMemory(CpuContext ctx)
{
var outAddress = ctx[CpuRegister.Rsi];
if (outAddress != 0)
{
// {size, alignment} — 1 MiB / 256 B covers the caller's allocation.
ctx.TryWriteUInt64(outAddress, 0x10_0000);
ctx.TryWriteUInt64(outAddress + sizeof(ulong), 0x100);
}
return ctx.SetReturn(Ok);
}
[SysAbiExport(Nid = "GrA9ke1QT+E", ExportName = "sceAudioPropagationSystemQueryInfo", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SystemQueryInfo(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "aNEqtSHdUSo", ExportName = "sceAudioPropagationSystemCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SystemCreate(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "x5VPqg5iyAk", ExportName = "sceAudioPropagationSystemDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SystemDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "ile38Gl-p5M", ExportName = "sceAudioPropagationSystem", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int System(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "cMl3u+7QBBM", ExportName = "sceAudioPropagationSystemMemoryInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SystemMemoryInit(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "3B9IabLByyM", ExportName = "sceAudioPropagationSystemOptionInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SystemOptionInit(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "B2KI2AachWE", ExportName = "sceAudioPropagationSystemLock", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SystemLock(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "kIdb+iQUzCs", ExportName = "sceAudioPropagationSystemSetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SystemSetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "VlBT16890mA", ExportName = "sceAudioPropagationSystemSetRays", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SystemSetRays(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "ht-QXT3zGxo", ExportName = "sceAudioPropagationSystemGetRays", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SystemGetRays(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "CPLV6G-eXmk", ExportName = "sceAudioPropagationSystemRegisterMaterial", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SystemRegisterMaterial(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "XKCN4gpeYsM", ExportName = "sceAudioPropagationSystemUnregisterMaterial", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SystemUnregisterMaterial(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "8bI5h8req30", ExportName = "sceAudioPropagationRoomCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int RoomCreate(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "S0JwP2AFTTE", ExportName = "sceAudioPropagationRoomDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int RoomDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "b-dYXrjSNZU", ExportName = "sceAudioPropagationPortalCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int PortalCreate(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "ZQXE-xS6MTE", ExportName = "sceAudioPropagationPortalDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int PortalDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "WXMhENV2NcA", ExportName = "sceAudioPropagationPortalSetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int PortalSetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "i687TNRF+hw", ExportName = "sceAudioPropagationPortalSettingsInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int PortalSettingsInit(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "d84otraxt2s", ExportName = "sceAudioPropagationSourceCreate", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceCreate(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "wkseM3LWPuc", ExportName = "sceAudioPropagationSourceDestroy", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceDestroy(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "-wsUTr31yeg", ExportName = "sceAudioPropagationSourceSetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceSetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "PBcrVpEqUVY", ExportName = "sceAudioPropagationSourceCalculateAudioPaths", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceCalculateAudioPaths(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "eEeKqFeNI3o", ExportName = "sceAudioPropagationSourceGetAudioPath", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceGetAudioPath(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "G+QLTfyLMYk", ExportName = "sceAudioPropagationSourceGetAudioPathCount", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceGetAudioPathCount(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "aKJZx7wCma8", ExportName = "sceAudioPropagationSourceGetRays", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceGetRays(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "3aEY9tPXGKc", ExportName = "sceAudioPropagationSourceQueryInfo", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceQueryInfo(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "hhz9pITnC8k", ExportName = "sceAudioPropagationSourceRender", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceRender(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "SoKPzY1-3SU", ExportName = "sceAudioPropagationSourceRenderInfoInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceRenderInfoInit(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "tKSmk2JsMAA", ExportName = "sceAudioPropagationSourceSetAudioPath", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceSetAudioPath(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "5vzOS2pHMFc", ExportName = "sceAudioPropagationSourceSetAudioPaths", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceSetAudioPaths(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "MNmGapXrYRs", ExportName = "sceAudioPropagationSourceSetAudioPathsParamInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int SourceSetAudioPathsParamInit(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "i-0aUex3zCE", ExportName = "sceAudioPropagationAudioPathInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int AudioPathInit(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "JZIkSbmt2BE", ExportName = "sceAudioPropagationAudioPathPointInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int AudioPathPointInit(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "tL2AEPejVQE", ExportName = "sceAudioPropagationPathGetNumPoints", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int PathGetNumPoints(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "2BSFmuKtRss", ExportName = "sceAudioPropagationMaterialInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int MaterialInit(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "0r2+9UTg1BA", ExportName = "sceAudioPropagationRayInit", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int RayInit(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "BbOT4vBwAjs", ExportName = "sceAudioPropagationResetAttributes", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int ResetAttributes(CpuContext ctx) => ctx.SetReturn(Ok);
[SysAbiExport(Nid = "gCmQm6dvMxw", ExportName = "sceAudioPropagationReportApi", Target = Generation.Gen5, LibraryName = "libSceAudioPropagation")]
public static int ReportApi(CpuContext ctx) => ctx.SetReturn(Ok);
}
+11 -250
View File
@@ -1109,7 +1109,7 @@ public static class AvPlayerExports
return null; return null;
} }
internal static string? ResolveGuestPath(string guestPath) private static string? ResolveGuestPath(string guestPath)
{ {
if (string.IsNullOrWhiteSpace(guestPath)) if (string.IsNullOrWhiteSpace(guestPath))
{ {
@@ -1117,39 +1117,13 @@ public static class AvPlayerExports
} }
var normalized = guestPath.Replace('\\', '/'); var normalized = guestPath.Replace('\\', '/');
var fileReference = normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase); if (Uri.TryCreate(normalized, UriKind.Absolute, out var uri) && uri.IsFile)
var unrealProjectRelative = false;
if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase) &&
Uri.TryCreate(normalized, UriKind.Absolute, out var uri) &&
uri.IsFile)
{ {
if (!string.IsNullOrEmpty(uri.Host) && normalized = uri.LocalPath;
!string.Equals(uri.Host, "localhost", StringComparison.OrdinalIgnoreCase))
{
return null;
}
normalized = uri.LocalPath.Replace('\\', '/');
} }
else if (normalized.StartsWith("file://", StringComparison.OrdinalIgnoreCase)) if (File.Exists(normalized))
{ {
// Some console middleware emits Unreal-style project-relative return Path.GetFullPath(normalized);
// media references such as file://../../../Project/Content/....
// System.Uri rejects these because the first ".." is parsed as
// an invalid authority. Treat the scheme as a guest-path marker;
// the app0 sandbox below resolves the relative path.
normalized = normalized["file://".Length..];
unrealProjectRelative = true;
}
else if (normalized.StartsWith("file:", StringComparison.OrdinalIgnoreCase))
{
normalized = normalized["file:".Length..];
unrealProjectRelative = true;
}
if (unrealProjectRelative)
{
normalized = RemoveUnrealLeadingDotSegments(normalized);
} }
var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR"); var app0 = Environment.GetEnvironmentVariable("SHARPEMU_APP0_DIR");
@@ -1157,232 +1131,19 @@ public static class AvPlayerExports
{ {
return null; return null;
} }
foreach (var prefix in new[] { "app0:/", "/app0/", "app0:", "/app0" })
var app0MountedPath = false;
foreach (var prefix in new[] { "app0:/", "/app0/", "app0/", "app0:" })
{ {
if (normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase)) if (normalized.StartsWith(prefix, StringComparison.OrdinalIgnoreCase))
{ {
normalized = normalized[prefix.Length..]; normalized = normalized[prefix.Length..];
app0MountedPath = true;
break; break;
} }
} }
var candidate = Path.GetFullPath(Path.Combine(app0, normalized.TrimStart('/')));
if (!app0MountedPath && var root = Path.GetFullPath(app0).TrimEnd(Path.DirectorySeparatorChar) + Path.DirectorySeparatorChar;
(string.Equals(normalized, "app0:", StringComparison.OrdinalIgnoreCase) || return candidate.StartsWith(root, StringComparison.OrdinalIgnoreCase) && File.Exists(candidate)
string.Equals(normalized, "/app0", StringComparison.OrdinalIgnoreCase) || ? candidate
string.Equals(normalized, "app0", StringComparison.OrdinalIgnoreCase))) : null;
{
normalized = string.Empty;
app0MountedPath = true;
}
try
{
if (fileReference)
{
if (!TryDecodeFileReference(normalized, out normalized))
{
return null;
}
}
else if (ContainsInvalidMediaPathCharacters(normalized))
{
return null;
}
if ((!fileReference &&
!app0MountedPath &&
Uri.TryCreate(normalized, UriKind.Absolute, out _)) ||
Path.IsPathFullyQualified(normalized) ||
normalized.StartsWith("/", StringComparison.Ordinal))
{
return null;
}
if (!TryNormalizeApp0RelativePath(normalized, out var relativePath) ||
relativePath.Length == 0)
{
return null;
}
var root = Path.GetFullPath(app0);
var candidate = Path.GetFullPath(Path.Combine(root, relativePath));
var relativeToRoot = Path.GetRelativePath(root, candidate);
if (Path.IsPathFullyQualified(relativeToRoot) ||
string.Equals(relativeToRoot, "..", StringComparison.Ordinal) ||
relativeToRoot.StartsWith(
".." + Path.DirectorySeparatorChar,
StringComparison.Ordinal))
{
return null;
}
return TryResolveSandboxedFile(root, relativePath, out var resolved)
? resolved
: null;
}
catch (Exception exception) when (exception is ArgumentException or
IOException or
NotSupportedException or
UnauthorizedAccessException or
UriFormatException)
{
return null;
}
}
private static string RemoveUnrealLeadingDotSegments(string guestPath)
{
while (guestPath.StartsWith("../", StringComparison.Ordinal) ||
guestPath.StartsWith("./", StringComparison.Ordinal))
{
guestPath = guestPath[(guestPath.IndexOf('/') + 1)..];
}
return guestPath;
}
private static bool TryDecodeFileReference(string encoded, out string decoded)
{
decoded = string.Empty;
for (var index = 0; index < encoded.Length; index++)
{
if (encoded[index] != '%')
{
continue;
}
if (index + 2 >= encoded.Length ||
!Uri.IsHexDigit(encoded[index + 1]) ||
!Uri.IsHexDigit(encoded[index + 2]))
{
return false;
}
var escapedByte = Convert.ToByte(encoded.Substring(index + 1, 2), 16);
if (escapedByte is (byte)'/' or (byte)'\\')
{
return false;
}
index += 2;
}
decoded = Uri.UnescapeDataString(encoded);
return !ContainsInvalidMediaPathCharacters(decoded);
}
private static bool ContainsInvalidMediaPathCharacters(string path) =>
path.IndexOfAny(['?', '#']) >= 0 || path.Any(char.IsControl);
private static bool TryNormalizeApp0RelativePath(
string guestPath,
out string relativePath)
{
var segments = new List<string>();
foreach (var segment in guestPath.TrimStart('/').Split(
'/',
StringSplitOptions.RemoveEmptyEntries))
{
if (segment == ".")
{
continue;
}
if (segment == "..")
{
if (segments.Count == 0)
{
relativePath = string.Empty;
return false;
}
segments.RemoveAt(segments.Count - 1);
continue;
}
segments.Add(segment);
}
relativePath = string.Join(Path.DirectorySeparatorChar, segments);
return true;
}
private static bool TryResolveSandboxedFile(
string root,
string relativePath,
out string resolved)
{
resolved = string.Empty;
var current = root;
var segments = relativePath.Split(
Path.DirectorySeparatorChar,
StringSplitOptions.RemoveEmptyEntries);
for (var index = 0; index < segments.Length; index++)
{
var exact = Path.Combine(current, segments[index]);
var finalSegment = index == segments.Length - 1;
string? match;
if (finalSegment ? File.Exists(exact) : Directory.Exists(exact))
{
match = exact;
}
else
{
if (!Directory.Exists(current))
{
return false;
}
match = null;
foreach (var entry in Directory.EnumerateFileSystemEntries(current))
{
if (!string.Equals(
Path.GetFileName(entry),
segments[index],
StringComparison.OrdinalIgnoreCase))
{
continue;
}
if (match is not null)
{
// A case-sensitive host can contain two names that are
// indistinguishable to the guest. Refuse an ambiguous
// media path instead of selecting one nondeterministically.
return false;
}
match = entry;
}
}
if (match is null ||
(finalSegment ? !File.Exists(match) : !Directory.Exists(match)))
{
return false;
}
if ((File.GetAttributes(match) & FileAttributes.ReparsePoint) != 0)
{
// App packages do not need host filesystem links. Refusing
// them keeps media resolution inside the configured app0
// tree even when a dump contains a symlink or junction.
return false;
}
current = match;
}
if (!File.Exists(current))
{
return false;
}
resolved = Path.GetFullPath(current);
return true;
} }
private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value) private static bool TryReadNullTerminatedUtf8(CpuContext ctx, ulong address, int maxLength, out string value)
+9 -14
View File
@@ -29,9 +29,10 @@ internal static class Bink2MovieBridge
private static bool _availabilityReported; private static bool _availabilityReported;
/// <summary> /// <summary>
/// Returns true only when movie skipping was explicitly requested. Without /// Returns true when the guest should receive a normal "file not found"
/// a host adapter the guest must be allowed to run the Bink implementation /// result for a Bink movie. This is the safe default without a decoder:
/// statically linked into its executable. /// games that treat movies as optional fall through to their next state
/// rather than submitting an empty Bink GPU texture forever.
/// </summary> /// </summary>
internal static bool ShouldSkipGuestMovie(string hostPath) => internal static bool ShouldSkipGuestMovie(string hostPath) =>
hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) && hostPath.EndsWith(".bk2", StringComparison.OrdinalIgnoreCase) &&
@@ -52,18 +53,12 @@ internal static class Bink2MovieBridge
return; return;
} }
var mode = ResolveMode(); if (ResolveMode() == MovieMode.Dummy)
if (mode == MovieMode.Dummy)
{ {
AttachDummyMovieLocked(hostPath); AttachDummyMovieLocked(hostPath);
return; return;
} }
if (mode != MovieMode.Native)
{
return;
}
var adapter = GetAdapterLocked(); var adapter = GetAdapterLocked();
if (adapter is null) if (adapter is null)
{ {
@@ -170,15 +165,16 @@ internal static class Bink2MovieBridge
return MovieMode.Skip; return MovieMode.Skip;
} }
// Prefer the optional host adapter when one is supplied. Otherwise let // With no SDK adapter present, returning "not found" makes optional
// the game's statically linked Bink implementation consume the file. // cinematics advance. Supplying either an explicit path or the normal
// side-by-side adapter enables native playback automatically.
if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) || if (!string.IsNullOrWhiteSpace(Environment.GetEnvironmentVariable("SHARPEMU_BINK2_BRIDGE")) ||
EnumerateAdapterCandidates().Any(File.Exists)) EnumerateAdapterCandidates().Any(File.Exists))
{ {
return MovieMode.Native; return MovieMode.Native;
} }
return MovieMode.Guest; return MovieMode.Skip;
} }
private static void AttachDummyMovieLocked(string hostPath) private static void AttachDummyMovieLocked(string hostPath)
@@ -339,7 +335,6 @@ internal static class Bink2MovieBridge
private enum MovieMode private enum MovieMode
{ {
Guest,
Skip, Skip,
Dummy, Dummy,
Native, Native,
-140
View File
@@ -1,140 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers;
using System.Numerics;
namespace SharpEmu.Libs.Gpu;
/// <summary>
/// The pool backing AGC-to-presenter ownership transfers, shared by every backend
/// (the AGC layer rents, the presenter returns, so both sides must use one pool).
/// Guest draw snapshots churn through a small set of 128 KiB-16 MiB size classes
/// thousands of times per second; the process-wide shared pool trims and
/// repartitions those large arrays aggressively under GC load, causing hundreds of
/// MiB/s of replacement byte[] allocations, so this pool is bounded and non-shared.
/// </summary>
internal static class GuestDataPool
{
public static ArrayPool<byte> Shared { get; } = new BoundedByteArrayPool(
maxArrayLength: 16 * 1024 * 1024,
maxCachedBytes: 256UL * 1024 * 1024,
maxArraysPerBucket: 8);
public static void Trim() => ((BoundedByteArrayPool)Shared).Trim();
private sealed class BoundedByteArrayPool : ArrayPool<byte>
{
private readonly object _gate = new();
private readonly int _maxArrayLength;
private readonly ulong _maxCachedBytes;
private readonly int _maxArraysPerBucket;
private readonly Dictionary<int, Stack<byte[]>> _cachedByBucket = [];
private readonly HashSet<byte[]> _leases =
new(System.Collections.Generic.ReferenceEqualityComparer.Instance);
private ulong _cachedBytes;
public BoundedByteArrayPool(
int maxArrayLength,
ulong maxCachedBytes,
int maxArraysPerBucket)
{
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArrayLength);
ArgumentOutOfRangeException.ThrowIfZero(maxCachedBytes);
ArgumentOutOfRangeException.ThrowIfNegativeOrZero(maxArraysPerBucket);
_maxArrayLength = maxArrayLength;
_maxCachedBytes = maxCachedBytes;
_maxArraysPerBucket = maxArraysPerBucket;
}
public override byte[] Rent(int minimumLength)
{
ArgumentOutOfRangeException.ThrowIfNegative(minimumLength);
var length = GetAllocationLength(minimumLength);
byte[]? array = null;
lock (_gate)
{
if (length <= _maxArrayLength &&
_cachedByBucket.TryGetValue(length, out var bucket) &&
bucket.TryPop(out array))
{
_cachedBytes -= (ulong)array.LongLength;
}
array ??= new byte[length];
_leases.Add(array);
}
return array;
}
public override void Return(byte[] array, bool clearArray = false)
{
ArgumentNullException.ThrowIfNull(array);
lock (_gate)
{
if (!_leases.Remove(array))
{
return;
}
}
if (clearArray)
{
Array.Clear(array);
}
lock (_gate)
{
if (array.Length > _maxArrayLength ||
!IsBucketLength(array.Length) ||
(ulong)array.LongLength > _maxCachedBytes -
Math.Min(_cachedBytes, _maxCachedBytes))
{
return;
}
if (!_cachedByBucket.TryGetValue(array.Length, out var bucket))
{
bucket = new Stack<byte[]>();
_cachedByBucket.Add(array.Length, bucket);
}
if (bucket.Count >= _maxArraysPerBucket)
{
return;
}
bucket.Push(array);
_cachedBytes += (ulong)array.LongLength;
}
}
public void Trim()
{
lock (_gate)
{
_cachedByBucket.Clear();
_cachedBytes = 0;
}
}
private int GetAllocationLength(int minimumLength)
{
if (minimumLength <= 16)
{
return 16;
}
if (minimumLength > _maxArrayLength)
{
return minimumLength;
}
return checked((int)BitOperations.RoundUpToPowerOf2((uint)minimumLength));
}
private static bool IsBucketLength(int length) =>
length >= 16 && (length & (length - 1)) == 0;
}
}
+2 -31
View File
@@ -1,7 +1,6 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.Libs.Gpu.Metal;
using SharpEmu.Libs.Gpu.Vulkan; using SharpEmu.Libs.Gpu.Vulkan;
namespace SharpEmu.Libs.Gpu; namespace SharpEmu.Libs.Gpu;
@@ -9,39 +8,11 @@ namespace SharpEmu.Libs.Gpu;
/// <summary> /// <summary>
/// Process-wide access point for the guest-GPU backend, mirroring HostPlatform for the /// Process-wide access point for the guest-GPU backend, mirroring HostPlatform for the
/// host seam: static HLE export classes resolve the renderer through <see cref="Current"/>. /// host seam: static HLE export classes resolve the renderer through <see cref="Current"/>.
/// Vulkan is the default everywhere; SHARPEMU_GPU_BACKEND=metal opts into the Metal /// Vulkan is the only backend today; Metal/DX12 slot in here.
/// backend (macOS only) while it is being brought up. macOS flips to Metal by default
/// once the presenter reaches parity.
/// </summary> /// </summary>
internal static class GuestGpu internal static class GuestGpu
{ {
private static readonly Lazy<IGuestGpuBackend> Instance = new(Create); private static readonly Lazy<IGuestGpuBackend> Instance = new(static () => new VulkanGuestGpuBackend());
public static IGuestGpuBackend Current => Instance.Value; public static IGuestGpuBackend Current => Instance.Value;
private static IGuestGpuBackend Create()
{
var requested = Environment.GetEnvironmentVariable("SHARPEMU_GPU_BACKEND");
if (string.IsNullOrEmpty(requested) || requested.Equals("vulkan", StringComparison.OrdinalIgnoreCase))
{
return new VulkanGuestGpuBackend();
}
if (requested.Equals("metal", StringComparison.OrdinalIgnoreCase))
{
if (!OperatingSystem.IsMacOS())
{
Console.Error.WriteLine(
"[LOADER][WARN] SHARPEMU_GPU_BACKEND=metal is only available on macOS; using Vulkan.");
return new VulkanGuestGpuBackend();
}
Console.Error.WriteLine("[LOADER][INFO] GPU backend: Metal (SHARPEMU_GPU_BACKEND).");
return new MetalGuestGpuBackend();
}
Console.Error.WriteLine(
$"[LOADER][WARN] Unknown SHARPEMU_GPU_BACKEND value '{requested}'; using Vulkan.");
return new VulkanGuestGpuBackend();
}
} }
+1 -25
View File
@@ -36,20 +36,6 @@ internal readonly record struct GuestSampler(
uint Word2, uint Word2,
uint Word3); uint Word3);
/// <summary>Identity of a texture's content in a backend texture cache, keyed
/// entirely on raw guest descriptor values; the AGC layer uses it to skip texel
/// copies for content the backend already holds.</summary>
internal readonly record struct TextureContentIdentity(
ulong Address,
uint Width,
uint Height,
uint Format,
uint NumberType,
uint DstSelect,
uint TileMode,
uint Pitch,
GuestSampler Sampler);
internal sealed record GuestMemoryBuffer( internal sealed record GuestMemoryBuffer(
ulong BaseAddress, ulong BaseAddress,
byte[] Data, byte[] Data,
@@ -136,22 +122,12 @@ internal readonly record struct GuestBlendState(
WriteMask: 0xFu); WriteMask: 0xFu);
} }
/// <summary>CB_BLEND_RED..ALPHA: the constant color referenced by the
/// CONSTANT_COLOR / CONSTANT_ALPHA blend factors. One constant serves every
/// render target of a draw; the hardware reset value is transparent black.</summary>
internal readonly record struct GuestBlendConstant(
float Red,
float Green,
float Blue,
float Alpha);
internal sealed record GuestRenderState( internal sealed record GuestRenderState(
IReadOnlyList<GuestBlendState> Blends, IReadOnlyList<GuestBlendState> Blends,
GuestRect? Scissor, GuestRect? Scissor,
GuestViewport? Viewport, GuestViewport? Viewport,
GuestRasterState Raster, GuestRasterState Raster,
GuestDepthState Depth, GuestDepthState Depth)
GuestBlendConstant BlendConstant = default)
{ {
public static GuestRenderState Default { get; } = new( public static GuestRenderState Default { get; } = new(
[GuestBlendState.Default], [GuestBlendState.Default],
-71
View File
@@ -1,7 +1,6 @@
// Copyright (C) 2026 SharpEmu Emulator Project // Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later // SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE;
using SharpEmu.ShaderCompiler; using SharpEmu.ShaderCompiler;
namespace SharpEmu.Libs.Gpu; namespace SharpEmu.Libs.Gpu;
@@ -18,10 +17,6 @@ namespace SharpEmu.Libs.Gpu;
/// </summary> /// </summary>
internal interface IGuestGpuBackend internal interface IGuestGpuBackend
{ {
/// <summary>Human-readable name of this backend ("Metal", "Vulkan"), shown in
/// the window title on macOS where either backend can run.</summary>
string BackendName { get; }
/// <summary>Starts the presenter (window + device) once; safe to call repeatedly.</summary> /// <summary>Starts the presenter (window + device) once; safe to call repeatedly.</summary>
void EnsureStarted(uint width, uint height); void EnsureStarted(uint width, uint height);
@@ -193,70 +188,4 @@ internal interface IGuestGpuBackend
/// the guest codes cross the seam and each backend maps them internally. /// the guest codes cross the seam and each backend maps them internally.
/// </summary> /// </summary>
bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind); bool TryGetRenderTargetOutputKind(uint dataFormat, uint numberType, out Gen5PixelOutputKind outputKind);
// Guest work ordering. AGC submissions execute on a single backend consumer in
// logical guest-queue order; sequences returned here are backend work tickets.
// A backend without a running presenter returns 0 from the Submit* methods and
// callers fall back to executing inline.
/// <summary>Scopes subsequent submissions on this thread to a named guest queue.</summary>
IDisposable EnterGuestQueue(string queueName, ulong submissionId);
/// <summary>Enqueues an action at its exact position in the current guest queue;
/// returns its work sequence, or 0 when nothing could be enqueued.</summary>
long SubmitOrderedGuestAction(Action action, string debugName);
/// <summary>Preserves sceAgcDcbWaitUntilSafeForRendering in queue order.</summary>
long SubmitOrderedGuestFlipWait(int videoOutHandle, int displayBufferIndex);
/// <summary>Blocks until the given work sequence completes; false on timeout,
/// close, or a non-positive sequence.</summary>
bool WaitForGuestWork(long workSequence, int timeoutMilliseconds = Timeout.Infinite);
/// <summary>Sequence currently executing on the guest-work consumer; diagnostics only.</summary>
long CurrentGuestWorkSequenceForDiagnostics { get; }
// Guest image lifecycle beyond presentation: CPU-visible seeding, writes, and
// extent queries the AGC layer uses to keep guest memory and backend images
// coherent. Addresses and formats are always raw guest values.
/// <summary>Whether the image exists on the backend or an already-queued upload
/// owns its initialization (a pending image may skip a duplicate upload but is
/// not yet a valid flip source).</summary>
bool IsGuestImageUploadKnown(ulong address, uint format, uint numberType);
/// <summary>True when the first draw into this address must seed the backend
/// image from guest memory (PS5 render targets alias guest memory, so
/// CPU-prefilled pixels are visible before the first draw).</summary>
bool GuestImageWantsInitialData(ulong address);
void ProvideGuestImageInitialData(ulong address, byte[] rgbaPixels);
void SubmitGuestImageFill(ulong address, uint fillValue);
void SubmitGuestImageWrite(ulong address, byte[] pixels);
bool TryGetGuestImageExtent(ulong address, out uint width, out uint height, out ulong byteCount);
IReadOnlyList<(ulong Address, uint Width, uint Height, ulong ByteCount)> GetGuestImageExtents();
/// <summary>Whether the backend's texture cache already holds this content; lets
/// the AGC layer skip copying texels out of guest memory on every draw.</summary>
bool IsTextureContentCached(in TextureContentIdentity identity);
/// <summary>Guest memory handle for backend self-healing (cache misses re-read
/// texels directly instead of showing a fallback pattern).</summary>
void AttachGuestMemory(ICpuMemory memory);
/// <summary>Alignment the AGC layer must apply to storage-buffer offsets before
/// they cross the seam.</summary>
ulong GuestStorageBufferOffsetAlignment { get; }
/// <summary>Counts a guest shader translation for the perf overlay.</summary>
void CountShaderCompilation();
(long Draws, double DrawMs, long Pipelines, long ShaderCompilations) ReadAndResetPerfCounters();
/// <summary>Asks a running presenter to close its window.</summary>
void RequestClose();
} }
@@ -1,28 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text;
using SharpEmu.ShaderCompiler.Metal;
namespace SharpEmu.Libs.Gpu.Metal;
/// <summary>
/// The Metal backend's compiled shader: MSL source plus the reflection data
/// (<see cref="Gen5MslShader"/>) the presenter needs to create and bind pipeline
/// states. The diagnostics payload is the source text — Metal has no portable
/// binary form until an MTLBinaryArchive is introduced.
/// </summary>
internal sealed class MetalCompiledGuestShader(Gen5MslShader shader) : IGuestCompiledShader
{
private byte[]? _payload;
public Gen5MslShader Shader { get; } = shader;
/// <summary>MTLLibrary handle cached by the presenter after the first
/// runtime compile; the render loop is its only reader and writer.</summary>
internal nint CachedLibrary;
public byte[] Payload => _payload ??= Encoding.UTF8.GetBytes(Shader.Source);
public string PayloadFileExtension => "msl";
}

Some files were not shown because too many files have changed in this diff Show More