Compare commits

..

No commits in common. "1016_part3" and "v0.0.3-release.4" have entirely different histories.

986 changed files with 55481 additions and 204176 deletions

Binary file not shown.

Before

Width:  |  Height:  |  Size: 219 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 356 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 398 KiB

After

Width:  |  Height:  |  Size: 345 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 485 KiB

After

Width:  |  Height:  |  Size: 229 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 538 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 693 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 392 KiB

View File

@ -12,7 +12,7 @@ on:
jobs:
dispatch:
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- name: Trigger sharpemu-site rebuild
env:

View File

@ -26,7 +26,7 @@ permissions:
jobs:
comment:
name: Post artifact links
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
# Only for successful PR builds — artifacts exist only when the build passed.
if: >-
github.event.workflow_run.event == 'pull_request' &&

View File

@ -34,7 +34,7 @@ concurrency:
jobs:
init:
name: Init
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
outputs:
release-name: ${{ steps.vars.outputs.release-name }}
release-tag: ${{ steps.vars.outputs.release-tag }}
@ -71,7 +71,7 @@ jobs:
reuse:
name: REUSE Compliance
needs: init
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
@ -80,7 +80,7 @@ jobs:
uses: fsfe/reuse-action@v6
build:
name: Build win-x64
name: Build Archive
needs:
- init
- reuse
@ -138,7 +138,7 @@ jobs:
fail-fast: false
matrix:
include:
- os: ubuntu-26.04
- os: ubuntu-latest
rid: linux-x64
- os: macos-latest
rid: osx-x64
@ -227,7 +227,7 @@ jobs:
# Versioned releases are immutable and tag-driven. Branch and manual runs
# still produce Actions artifacts without modifying a published release.
if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v')
runs-on: ubuntu-26.04
runs-on: ubuntu-latest
permissions:
contents: write
steps:

View File

@ -9,7 +9,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ImplicitUsings>enable</ImplicitUsings>
<Nullable>enable</Nullable>
<GenerateDocumentationFile>true</GenerateDocumentationFile>
<SharpEmuVersion>0.0.5-nexus</SharpEmuVersion>
<SharpEmuVersion>0.0.3-release.4</SharpEmuVersion>
<Version>$(SharpEmuVersion)</Version>
<RepoRoot>$([MSBuild]::NormalizeDirectory('$(MSBuildThisFileDirectory)'))</RepoRoot>

View File

@ -22,10 +22,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<PackageVersion Include="Silk.NET.Vulkan" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.EXT" Version="2.23.0" />
<PackageVersion Include="Silk.NET.Vulkan.Extensions.KHR" Version="2.23.0" />
<!-- Reuse the font rasterizer already shipped with Avalonia.Skia. -->
<PackageVersion Include="SkiaSharp" Version="3.119.4" />
<PackageVersion Include="SkiaSharp.NativeAssets.Linux" Version="3.119.4" />
<PackageVersion Include="System.IO.Hashing" Version="10.0.0" />
<!-- Transitive of Avalonia.Desktop; pinned. Avalonia 12 requires 0.94.1+. -->
<PackageVersion Include="Tmds.DBus.Protocol" Version="0.94.1" />
<PackageVersion Include="xunit" Version="2.9.3" />

View File

@ -15,18 +15,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
---
<p align="center">
<a href="https://discord.gg/zdTuUU9Uwn">
<img src="https://img.shields.io/badge/Discord-Join%20our%20Community-5865F2?style=for-the-badge&logo=discord&logoColor=white" alt="Join our Discord">
</a>
</p>
<p align="center">
<strong>Join our official Discord server for development updates, compatibility discussions, support, and community chat.</strong>
</p>
---
<p align="center">
<a href="#support">
<img src="https://img.shields.io/badge/Support-GitHub%20Sponsors%20%26%20Crypto-EA4AAA?style=for-the-badge&logo=githubsponsors&logoColor=white" alt="Support SharpEmu">
@ -35,9 +23,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
---
> [!IMPORTANT]
> **Official channels:** Only **sharpemu.app** and the links listed in this GitHub repository are affiliated with SharpEmu. Any other websites, accounts, or donation pages are unofficial and unauthorized.
> [!NOTE]
> 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
@ -58,21 +43,32 @@ Our goal is **not** to emulate PS4 games, as there is already an excellent emula
## Games Tested
| Demon's Souls Remake | ASTRO BOT |
| Demons Souls Remake | Dreaming Sarah |
| :-----------------------------------------------------------: | :--------------------------------------------------------------------------------------------: |
| ![DeS screenshot](./.github/images/demons-souls.jpg) | ![Astro Bot](./.github/images/astro-bot.jpg) |
| ![Bloodborne screenshot](./.github/images/demons-souls.jpg) | ![Dreaming Sarah](./.github/images/dreaming-sarah.jpg) |
| ASTRO's PLAYROOM | Dead Cells |
| Void Terrarium | Dead Cells |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![Astro's Playroom](./.github/images/astros-playroom.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
| God of War: Sons of Sparta | Tomb Raider V Remastered |
| :------------------------------------------------------------------------: | :------------------------------------------------------------------: |
| ![GoW SOS](./.github/images/gow-sos.jpg) | ![Tomb Raider V](./.github/images/tomb-raider-v-remastered.jpg) |
| ![Void Terrarium](./.github/images/void-terrarium.jpg) | ![Dead Cells](./.github/images/dead-cells.jpg) |
## Status
The emulator can currently load the `eboot.bin` of real games, execute native CPU instructions, and partially handle gpu-related functionality. Included 3D games.
The emulator can currently load the `eboot.bin` of real games, execute native CPU instructions, and partially handle kernel-related functionality. However, several critical components are still missing.
Current capabilities include:
* Loading `eboot.bin` and `.elf` files
* Executing native CPU instructions
* Reading basic game metadata (title, version, etc.)
* Loading system modules (`prx` / `sys_module`)
* Partial support for some kernel functions
* `Fiber` and `AMPR` exports
* PlayGo scenarios
* Initial loading game files
* Shader/resource submits and AGC initial
* Video outputs in some games
Some games have reached like `sceVideoOut` and AGC stages.
SharpEmu supports Windows, Linux, and macOS hosts. Video output uses Vulkan on
Windows and Linux, and MoltenVK on macOS. Platform support is still experimental,
@ -83,12 +79,11 @@ so compatibility and performance vary by game, operating system, and GPU driver.
Download the release archive for your operating system, extract it, and launch
SharpEmu with the path to a legally obtained game's `eboot.bin`.
Or command line;
Windows PowerShell:
```powershell
.\SharpEmu.exe "C:\path\to\game\eboot.bin" --log-to-file
.\SharpEmu.exe "C:\path\to\game\eboot.bin" 2>&1 |
Tee-Object -FilePath "SharpEmu.log"
```
Linux and macOS:
@ -96,13 +91,10 @@ Linux and macOS:
```bash
chmod +x ./SharpEmu
./SharpEmu "/path/to/game/eboot.bin" --log-to-file
./SharpEmu "/path/to/game/eboot.bin" 2>&1 |
tee SharpEmu.log
```
SharpEmu supports environment variables that can be configured from the GUI. Undocumented variables are listed here: [docs/sharpemu-gui-undocumented-env-vars.md](docs/sharpemu-gui-undocumented-env-vars.md)
You can set them per game or globally in the GUI, or pass them directly through the CLI.
A Vulkan-capable GPU and current graphics driver are required. The macOS
release includes the MoltenVK Vulkan implementation.

View File

@ -12,9 +12,6 @@ python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk
# Export name to NID
python scripts/aerolib_catalog.py lookup sceKernelWaitSema
# A comma can be used for multiple query strings.
python scripts/aerolib_catalog.py lookup Zxa0VhQVTsk,SaKib2Ug0yI,sceVideoOutOpen,amuBfI-AQc4
# Search export names
python scripts/aerolib_catalog.py search VideoOut --limit 20

View File

@ -9,12 +9,12 @@ Demon's Souls plays Bink 2 (.bk2) files through a Bink implementation linked
directly into eboot.bin. It does not use libSceVideodec, therefore an HLE video
decoder cannot observe or replace those frames.
By default, SharpEmu leaves Bink decoding to the implementation linked into
the game (`SHARPEMU_BINK_MODE=guest`).
SharpEmu observes successful guest .bk2 opens and, when a Bink decoder is
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
the movie without trying to execute the PS5-specific Bink GPU decode path.
Set `SHARPEMU_BINK_MODE=native` to use the optional host bridge. It observes
guest .bk2 opens and presents decoded frames at the guest-flip boundary. The
host path decodes by calling FFmpeg's C API directly from managed
The default path decodes by calling FFmpeg's own C API directly from managed
code (`src/SharpEmu.Libs/Bink/FfmpegNativeBinkFrameSource.cs`, via the
[FFmpeg.AutoGen](https://github.com/Ruslan-B/FFmpeg.AutoGen) P/Invoke
bindings) against a custom FFmpeg build
@ -24,13 +24,26 @@ libraries come from. No proprietary RAD SDK is needed to build or run
SharpEmu, and there is no C/C++ code of SharpEmu's own involved in decoding
-- SharpEmu.CLI.csproj only downloads a prebuilt release archive.
Set `SHARPEMU_BINK_MODE=skip` only when explicitly testing a title whose
cinematics are optional.
Set `SHARPEMU_BINK_MODE=guest` to leave decoding to the Bink implementation
statically linked into the game instead. Set `skip` only when explicitly
testing a title whose cinematics are optional.
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
only; it does not decode the movie or alter its game logic.
`SHARPEMU_BINK_MODE=ffmpeg` is an alias for `native`.
SHARPEMU_BINK_MODE=native is equivalent to the default and mainly useful for
being explicit about it.
The experimental `SHARPEMU_BINK_MODE=ffmpeg` override is unrelated to the
default path above: instead of calling into FFmpeg in-process, it spawns a
standalone `ffmpeg` executable and reads raw frames from its stdout
(`src/SharpEmu.Libs/Bink/FfmpegBinkFrameSource.cs`). SharpEmu searches
`SHARPEMU_FFMPEG_PATH`, the executable directory, its `ffmpeg` subdirectory,
and then `PATH` (plus a couple of common Homebrew paths on macOS). That
`ffmpeg` build must contain a Bink 2 decoder itself; a stock FFmpeg build that
only recognizes the Bink container is not sufficient. Most users want the
default `native` mode instead, which always has Bink 2 support since it's
built against `ffmpeg-core` specifically.
## Supplying the FFmpeg libraries

View File

@ -1,80 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Shader rejection diagnostics
`SHARPEMU_STRICT_COMPUTE` controls the rejections listed below for compute dispatches
and graphics draws. The existing variable name is retained; there is no separate
`SHARPEMU_STRICT_DRAW` switch. Restart the game after a change. The pipeline cache and
draw executor read the value when created.
- Unset or `1`: stop on the rejection so that the failure can be diagnosed (default).
- `0`: skip the affected dispatch or draw. Only this value enables skipping.
The GUI provides this switch under **Environment > Debug**, globally and per game.
It is on by default. Turning it off saves an explicit `0`; an inherited process
environment value does not override the GUI choice. Other values retain strict mode.
## Complete skip list
With the switch set to `0`, these are the only handled rejection categories:
| Rejection | Detection point | Operation skipped |
| --- | --- | --- |
| Resource-plan extraction throws `ResourcePlanException`, including an unsupported resource expression or a value graph that does not converge | `ShaderProgramCache.CreateEntry` | The dispatch or draw that needs the rejected shader |
| Shader compilation returns failure or no compiled program, including an unsupported opcode reported by the compiler | `ShaderProgramCache.CompilePermutation` | The dispatch or draw that needs the rejected shader |
| Indirect image candidates have incompatible image classes (`IncompatibleImageCandidates`) | `ResourceMaterializer.Materialize`, handled by `ShaderProgramCache` | The dispatch or draw that needs those resources |
| Indirect image expansion exceeds the dense image resource limit (`ImageCapacityExceeded`) | `ResourceMaterializer.Materialize`, handled by `ShaderProgramCache` | The dispatch or draw that needs those resources |
| A graphics texture's cached image type cannot support the requested view type, such as a 2D view of a 1D image | Vulkan `ValidateDrawImageTypes`, handled by `RenderExecutor.RecordDraw` | The whole indexed or automatic draw |
The last category applies to vertex and pixel resource preparation, not compute
image binding. It checks available, non-stale cache entries and excludes host movie
textures. A mismatch detected later in binding remains fatal.
The draw image-type check runs before resource binding and draw recording. Skip mode
does not create an invalid view or issue the rejected draw. The preparation scope is
released, and the caller resets the image bindings before the next operation.
## Failures that remain fatal
- Shader decoding failures and invalid shader source reads.
- Unreadable resource memory, invalid indirect table state, and materialization failures
other than the two named categories above.
- Invalid binding layouts or invalid resource snapshot state.
- Other image checks, including format, usage, aspect, component mapping, and range
checks reached during binding or view creation. The image cache checks are unchanged.
- Image-type mismatches in compute binding or outside the graphics preparation check.
- Host allocation failures, device loss, synchronization failures, and unexpected exceptions.
This is not a general exception bypass. Setting the switch to `1` also makes every
category in the skip list fatal. Supported operations use the same path in both modes.
## Warnings and test limits
Program rejections log `COMPUTE_SKIPPED` or `DRAW_SKIPPED` once per stage, shader hash,
and code size in each pipeline cache. Draw image-type rejections log `DRAW_SKIPPED`
once per shader hash, image type, and view type in each draw executor. Warning counts
are not skipped-operation counts. No rejected permutation is recorded as compiled;
a later request can retry it.
A skipped operation does not produce its outputs. Missing images, incorrect results,
later errors, and stalls are possible. FPS can rise because less work is executed.
Skip-mode smoke tests can check progress and expose other defects, but must record
the skip warnings. They do not close the skipped defects or establish a final visual
or performance baseline. This switch does not restore the old renderer.
## PowerShell
Enable strict testing in PowerShell:
```powershell
$env:SHARPEMU_STRICT_COMPUTE = '1'
```
Explicitly enable skip mode:
```powershell
$env:SHARPEMU_STRICT_COMPUTE = '0'
```

View File

@ -1,54 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Performance display
Open **Options → Rendering**, globally or for one game.
- **Performance display:** start with statistics on or off.
- **Display mode:** Full shows all counters and the frame graph. Minimal shows
guest FPS, process CPU usage, process GPU usage, and elapsed session time.
Title bar shows the same minimal values in the window title.
- **Screen corner:** choose any of the four corners. This setting applies only
to the on-screen panel.
F1 toggles the display for the current run. F2 cycles clockwise through the
screen corners. Neither key changes saved settings. The next run uses the saved
options again. Command+F1 retains its separate Metal HUD shortcut.
`SHARPEMU_OVERLAY=0` starts the display hidden, even when the saved option is on.
F1 can still show it. RenderDoc's F12 hint remains in the title when available.
The operating system does not show a title bar in borderless/fullscreen modes.
The launcher passes these options to the game process, including when a separate
emulator path is configured:
```text
--overlay=on|off
--overlay-mode=full|minimal|titlebar
--overlay-corner=topleft|topright|bottomright|bottomleft
```
## Usage values
The panel dimensions are separate constants in `PerfOverlay.cs`:
`PanelWidth` / `PanelHeight` for Full, and
`MinimalPanelWidth` / `MinimalPanelHeight` for Minimal. Both heights are fixed;
the renderer clips the panel only when the window is too small.
CPU usage is the emulator process's CPU time divided by wall time and the
number of logical processors. GPU usage is the busiest GPU engine used by the
emulator process, not GPU memory usage or total usage by all applications.
Windows supplies the GPU Engine performance counters. Collection runs off the
render thread and uses the existing statistics refresh interval. Rate counters
need two samples. Missing counters, unavailable samples, and other platforms
show **N/A**, not zero. No GPU waits or presentation delays are added.
The full panel keeps cache memory counters and puts CPU/GPU usage on a separate
line above elapsed time and the Vulkan allocation counters.
The counter interpretation follows [Microsoft's GPU utilization description](https://devblogs.microsoft.com/directx/gpus-in-the-task-manager/).
Wildcard samples use [PdhGetFormattedCounterArray](https://learn.microsoft.com/en-us/windows/win32/api/pdh/nf-pdh-pdhgetformattedcounterarrayw).

View File

@ -1,433 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# SharpEmu: environment variables that are not on the GUI options pages
## Scope
This list contains each `SHARPEMU_*` environment variable in the repository that the GUI does not show.
The GUI shows 14 variables on its **Options** and **Game options** pages. This list does not contain them:
`SHARPEMU_BTHID_UNAVAILABLE`, `SHARPEMU_CRASH_CAPTURE`, `SHARPEMU_DEFAULT_PROFILE`,
`SHARPEMU_DISABLE_IMPORT_LOOP_GUARD`, `SHARPEMU_DUMP_SPIRV`, `SHARPEMU_LOG_DIRECT_MEMORY`, `SHARPEMU_LOG_IO`,
`SHARPEMU_LOG_NP`, `SHARPEMU_PROFILE_PERFORMANCE`, `SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE`,
`SHARPEMU_RENDERDOC`, `SHARPEMU_VK_DISABLE_IMPLICITS`, `SHARPEMU_STRICT_COMPUTE`, `SHARPEMU_VK_VALIDATION`, `SHARPEMU_WRITABLE_APP0`.
The list contains 269 variables. The source was examined on 2026-09-19, branch `dev`.
The descriptions come from the code that reads each variable. The emulator was not started for this list.
## How to use a variable
1. Set the variable in the shell before you start the emulator or the GUI.
2. You can also enter additional env vars using the button next to the "Play" button from GUI.
3. The GUI also accepts a variable in the `EnvironmentToggles` list of its settings file. Write `NAME` or `NAME=value`. An entry without a value gets the value `1`.
4. The emulator reads most variables one time, when it starts. Start the game again after a change.
5. If the **Value** column shows `1`, only the value `1` turns the function on, unless the row gives other values.
## Number of variables in each group
| Group | Variables |
| --- | --- |
| Behavior switches | 19 |
| GPU and Vulkan | 59 |
| Metal | 10 |
| Audio and video | 6 |
| Input | 5 |
| Memory and CPU | 19 |
| Loader | 11 |
| Debugger | 1 |
| Log channels | 72 |
| Traces | 25 |
| Data dumps | 12 |
| Performance measurement | 17 |
| Internal | 1 |
| Tests and tools | 12 |
## Behavior switches
These variables change how the emulator operates.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_AGC_VERSION13_DEFAULTS` | text (`legacy`, `generic` or `0`) | Selects the register defaults for AGC version 13 requests. The default uses the version 11 compact tables. The values `legacy`, `generic` and `0` use the older generic layout. | `AgcExports.Init.cs` |
| `SHARPEMU_DISABLE_LLE_LIBC` | `1` | Stops the use of the guest libc module for libc exports. The emulator then uses its HLE handlers for these exports. | `DirectExecutionBackend.cs` |
| `SHARPEMU_HLE_FAST_MEMCPY` | `0` | Set to `0` to stop the native fast-path stub for the guest libc `memcpy` and `memmove`, so those imports use the managed HLE gateway again. | `DirectExecutionBackend.FastPath.cs` |
| `SHARPEMU_HLE_FAST_PATH` | `0` | Set to `0` to stop the native fast-path stubs for `pthread_self` and `pthread_getspecific`, so those imports use the managed HLE gateway again. The stubs are on by default on macOS x64 and are off on any run that traces imports or pthreads. | `DirectExecutionBackend.FastPath.cs` |
| `SHARPEMU_HOLD_FIRST_FLIP_MS` | number | Stops the thread that submits the flip for this number of milliseconds on one guest flip. The range is 0 to 60000 and the default is 0 (off). `SHARPEMU_HOLD_FLIP_NUMBER` selects the flip. | `VideoOutExports.cs` |
| `SHARPEMU_HOLD_FLIP_NUMBER` | number | Selects the guest flip that `SHARPEMU_HOLD_FIRST_FLIP_MS` holds. The minimum is 1 and the default is 1 (the first flip). | `VideoOutExports.cs` |
| `SHARPEMU_IGNORE_GUEST_EXCEPTIONS` | `1` | `sceKernelRaiseException` returns OK and does not call the installed guest exception handler. Default is off. | `KernelExceptionCompatExports.cs` |
| `SHARPEMU_IMPORT_LOOP_GUARD_SECONDS` | number | When `SHARPEMU_DISABLE_IMPORT_LOOP_GUARD=0` enables the guard, sets how long an import call pattern can repeat before a guest exit. The default is 5 seconds; 0 turns the guard off. | `DirectExecutionBackend.Imports.cs`, `DirectExecutionBackend.cs` |
| `SHARPEMU_NET_REDIRECT` | IP address | Sends guest `connect` and `sendto` traffic to this host IP address. When unset, the emulator permits outbound guest traffic only to loopback addresses. If the value is not a valid IP address, the emulator keeps the original destination and permits the traffic. | `KernelSocketCompatExports.cs` |
| `SHARPEMU_NO_FLIP_PACING` | `1` | Stops flip pacing. The emulator completes each flip immediately and does not wait for the display refresh time. The default is off. | `VideoOutExports.cs`, `VideoOutExports.FlipRequests.cs` |
| `SHARPEMU_OVERLAY` | `0` | Hides the performance overlay at start, even when the video options have the overlay on. All other values use the video options. | `PerfOverlay.cs`, `PerformanceOverlayState.cs` |
| `SHARPEMU_POSIX_GPU_FAULT_FAST_PATH` | `0` | Set to `0` to stop the POSIX fault bridge from offering GPU-tracked page faults to the GPU memory tracker before it builds the Win64 exception context. The fault then takes the full handler order again. | `DirectExecutionBackend.PosixSignals.cs` |
| `SHARPEMU_RETAIN_SUBMITTED_INDEX_DATA` | `0` | The default is on. The emulator keeps a copy of submitted index data until the translated draws run. Set to `0` to stop this copy. Use `0` only for a comparison test. | `AgcExports.SubmittedGeometry.cs` |
| `SHARPEMU_RETAIN_SUBMITTED_VERTEX_DATA` | `0` | The default is on. The emulator keeps a copy of submitted vertex data until the translated draws run. Set to `0` to stop this copy. Use `0` only for a comparison test. | `AgcExports.SubmittedGeometry.cs` |
| `SHARPEMU_SAVEDATA_DIR` | path | Set the root directory for save data. The default is `user/savedata` next to the executable. When this variable is set, the emulator does not move save data from the old layout. | `SaveDataStorage.cs`, `SaveDataExports.cs` |
| `SHARPEMU_STALL_WATCHDOG_SECONDS` | number (seconds) | Sets the time without import progress before the stall watchdog operates. The default is 20. The watchdog writes a snapshot and stops the process with exit code 4. It does not operate when a guest thread is ready or a known wait is in progress. `0` stops the watchdog. | `DirectExecutionBackend.cs` |
| `SHARPEMU_STRICT_RWLOCK_WRITER_PREFERENCE` | `1` | Set to `1` to make a guest thread take a free pthread rwlock as the exclusive writer. By default, the thread gets a compatibility writer record and the lock has no writer thread. | `KernelPthreadExtendedCompatExports.cs` |
| `SHARPEMU_TEMP0_DIR` | path | Set the host directory for the guest `temp0` mount. The default is `user/temp/<app name>/temp0` next to the executable. When unset, the kernel code sets this variable to the default path. | `AppContentExports.cs`, `KernelMemoryCompatExports.cs` |
| `SHARPEMU_TRACE_TITLE_SHADER_STATE` | `1` | The SPIR-V translator changes the pixel shader at address 0x0000000500781200 for diagnosis. The first float output then shows the execution state, the wave mask state, and the export state as colors. Default is off. | `Gen5SpirvTranslator.cs` |
## GPU and Vulkan
These variables change the GPU path or the Vulkan presenter.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_ASYNC_GUEST_READBACK` | `0` | `0` makes a guest thread that reads GPU-written buffer memory wait until the render thread has copied the data back. The default starts the copy on the render thread and lets the guest thread wait for it on a separate readback slot, so the render thread continues. | `GuestBufferCache.cs` |
| `SHARPEMU_CAPTURE_PIXEL_EXEC_PCS` | list (`pc:register` pairs) | At each given instruction PC, the pixel shader writes the execution flag to the given VGPR register. The value is 1.0 when the flag is set and 0.0 when it is not. `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_IMAGE_ADDRESS` | hex address | Selects the pixel shader for the image capture by its program address. The default is no capture. Use it with `SHARPEMU_CAPTURE_PIXEL_IMAGE_PC`. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_IMAGE_PC` | number (instruction PC) | Sets the image instruction whose result the SPIR-V translator copies to four VGPR registers. `SHARPEMU_CAPTURE_PIXEL_IMAGE_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_IMAGE_VGPR_BASE` | number (0 to 252) | Sets the first VGPR register that receives the captured image components. The default is 248. Values above 252 use the default. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` | hex address | Selects the pixel shader for the VGPR capture, execution capture and path marker functions by its program address. The default is no capture. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_DEST_BASE` | number (0 to 252) | Sets the first destination VGPR register for `SHARPEMU_CAPTURE_PIXEL_VGPR_SOURCES`. The default is 248. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_IGNORE_EXEC` | `1` | Makes the VGPR capture copies occur without the execution flag guard. The default applies the guard. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_PC` | number (instruction PC) | Sets the instruction PC at which the translator copies the source VGPR registers. `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_POINTS` | list (`pc:source:destination` triples) | At each given instruction PC, the pixel shader copies the source VGPR register to the destination register. Register numbers must be less than 256. `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS` must match the shader address. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_CAPTURE_PIXEL_VGPR_SOURCES` | list (one to four register numbers) | Gives the source VGPR registers that the translator copies at `SHARPEMU_CAPTURE_PIXEL_VGPR_PC`. The copies go to the registers that start at the destination base. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_DETILE` | `1` or `0` | Controls the detile step for tiled surfaces. The default detiles only the trusted swizzle modes. `1` also detiles the approximate modes and `0` uploads all surfaces as raw data. | `GnmTiling.cs` |
| `SHARPEMU_DISABLE_FILL_CLEAR` | `1` | Stops the adaptation that treats a transparent premultiplied fill draw as a target overwrite. The default keeps the adaptation on. | `ShaderPipelineCache.Adaptations.cs` |
| `SHARPEMU_ENABLE_CHUNKED_DRAWS` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT` | number (attribute location) | The Vulkan presenter reads the number into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_DEFAULT_RASTER_STATE` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_FULLSCREEN_PIPELINE` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_FULLSCREEN_VERTEX` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_PACKED_EXPORT_ONE` | `1` | The SPIR-V translator replaces each packed half-float export component with the constant 1.0. This applies to all shaders. Default is off. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PACKED_EXPORT_STORE_ONE` | `1` | The SPIR-V translator writes (1.0, 1.0) to the packed half-float pair before the export reads it. This applies only to the pixel shader at guest address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PACKED_STORE_EXEC_VALUES` | `1` | Each packed half-float store writes 1.0 when the EXEC mask is active and 0.5 when it is not. This shows the EXEC state as a color. This applies only to the pixel shader at guest address 0x0000000500781200. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PIXEL_EXPORT_ADDRESS` | hex address | Sets the shader address filter for the pixel export debug overrides. The value is a hex address, and the `0x` prefix is optional. When unset, the overrides apply to all pixel shaders. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PIXEL_EXPORT_PACK_VGPR_BASE` | number | Float pixel outputs read four vector registers that start at this base plus four times the export target. The translator packs the values to half-float and unpacks them again. The address filters select the shaders. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PIXEL_EXPORT_VGPR_ADDRESS` | hex address | Sets the shader address filter for the two VGPR base overrides. The `0x` prefix is optional. When unset, the filter from `SHARPEMU_FORCE_PIXEL_EXPORT_ADDRESS` applies. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PIXEL_EXPORT_VGPR_BASE` | number | Float pixel outputs read four vector registers that start at this base plus four times the export target. These values replace the decoded export sources. The address filters select the shaders. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_PIXEL_MAGENTA` | `1` | Each selected pixel shader export writes opaque magenta. The control flow, EXEC mask, geometry and raster state stay the same. Use `SHARPEMU_FORCE_PIXEL_EXPORT_ADDRESS` to select one shader. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_SOLID_FRAGMENT` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_TITLE_COMPARE_4D4` | `1` | The compare instruction at shader PC 0x4D4 always gives the result true. This applies only to the shader at guest address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.Alu.cs` |
| `SHARPEMU_FORCE_TITLE_COMPARE_540` | `1` | The compare instruction at shader PC 0x540 always gives the result true. This applies only to the shader at guest address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.Alu.cs` |
| `SHARPEMU_FORCE_TITLE_DEFAULT_RASTER_STATE` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_TITLE_EARLY_COLOR` | `1` | The pixel shader at guest address 0x0000000500781200 writes a constant to its first output and returns at once. A float output gets opaque magenta. An integer output gets zero. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_TITLE_EXPORT_EXEC` | `1` | The translator sets the EXEC mask to true and writes 1 to 64-bit register 126 before each pixel export store. This applies only to the pixel shader at guest address 0x0000000500781200. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_TITLE_FULLSCREEN_VERTEX` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_TITLE_SINGLE_MRT` | `1` | The translator declares only the first pixel output for the pixel shader at guest address 0x0000000500781200. It does not declare the other render target outputs. Default is off. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_FORCE_TITLE_SOLID_FRAGMENT` | `1` | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect at this time. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_FORCE_TITLE_VERTEX_OUTPUTS_ONE` | `1` | Vertex export targets 32 to 35 write the vector (1, 1, 1, 1). This applies only to the vertex shader at guest address 0x0000000500780000. Default is off. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_GPU_BACKEND` | `vulkan` or `metal` | Selects the guest GPU backend. The default is Vulkan. The value `metal` works only on macOS, and other hosts show a warning and use Vulkan. | `GuestGpu.cs` |
| `SHARPEMU_GPU_WAIT_SPIN_US` | number (microseconds) | Sets how long a wait for GPU work polls before it blocks. A short poll saves the wake-up delay when the GPU finishes soon. `0` blocks at once. The default is 150. | `GpuWaitSpin.cs` |
| `SHARPEMU_GRAPHICS_SUBGROUPS` | `0` or `1` | Set `1` to use native Vulkan subgroup operations in graphics shaders. Set `0` to use the fallback path. When unset, the presenter uses them only if the subgroup size is 32 and the vertex and fragment stages support subgroups. | `VulkanVideoPresenter.cs`, `VulkanVideoPresenter.Device.Setup.cs` |
| `SHARPEMU_IMAGE_LOOKUP_MEMO` | `0` | `0` turns off the image lookup memo. The default keeps the result of each image lookup that found an image with the same backing, and gives it again for the same request until an image is registered or removed. | `GuestImageCache.Lookup.cs` |
| `SHARPEMU_MARK_PIXEL_PCS` | list of `pc:register` pairs | Makes the SPIR-V pixel shader write the float value 1.0 to a shader register when execution reaches the given instruction PC. Use decimal numbers, a register below 256, and a comma between pairs. It has an effect only for the shader address in `SHARPEMU_CAPTURE_PIXEL_VGPR_ADDRESS`. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_MAX_GUEST_WORK_PER_RENDER` | number | Sets the maximum number of guest command stream slices that the Vulkan presenter runs in one render pass. The default is 1024, or 256 on macOS. The value must be more than 0. | `VulkanVideoPresenter.cs`, `VulkanVideoPresenter.CommandStream.cs` |
| `SHARPEMU_RENDERDOC_CAPTURE_TIMEOUT_SECONDS` | number (seconds) | Set the maximum time for a RenderDoc frame capture. The emulator discards a capture that does not end in this time. The range is 1 to 120. The default is 15. | `RenderDocCapture.cs` |
| `SHARPEMU_RENDERDOC_DLL` | path | Set the path of the RenderDoc library file. The emulator tries this path first. When unset or when the load fails, it searches the default library name and the known install paths. | `RenderDocCapture.cs` |
| `SHARPEMU_RENDERDOC_WAIT` | number (seconds) or `enter` | Set to make the emulator wait before it starts Vulkan, so that you can attach RenderDoc. `enter` waits until you press Enter. A number waits that many seconds, from 1 to 300. Other text waits 15 seconds. | `VulkanVideoPresenter.Device.Setup.cs` |
| `SHARPEMU_RENDER_WORK_BUDGET_MS` | number (milliseconds) | Set the maximum time that one render call uses for queued guest work in the Vulkan presenter. Work that remains stays in the queue for the next frame. `0` removes the limit. The default is 12 on macOS and 0 on other hosts. | `VulkanVideoPresenter.cs`, `VulkanVideoPresenter.RenderLoop.cs` |
| `SHARPEMU_RESOURCE_PREFETCH` | `0` | `0` makes the shader resource materialization read each guest word one at a time. The default reads adjacent words of descriptors and the flattened resource table in one read of resident guest memory. A range that the GPU may still own is read word by word as before. | `ShaderProgramCache.cs`, `RawReadPrefetch.cs` |
| `SHARPEMU_SHADER_MAX_STEPS` | number | Set the maximum number of dispatcher loop steps in each translated shader invocation. The limit makes sure that an incorrect loop stops. `0` removes the limit. The default is 100000. | `Gen5SpirvTranslator.cs`, `Gen5MslTranslator.cs` |
| `SHARPEMU_SHADER_PREWARM` | `0` | `0` turns off the shader prewarm list. By default, each compute program that a game compiles is added at once to `shader-prewarm.bin` in `user/pipeline_cache/<title id>/`, so a stopped game keeps the list. At the next launch after a change of the shader translator or of the GPU driver, background threads translate the listed programs again and create their pipelines, so that the driver compiles them before the game asks for them. The `shader-prewarm.stamp` file names the build and driver that last did this; with the same build and driver, the launch does no prewarm work. The log shows `Shader prewarm done` and `Shader prewarm hits`. On Windows it replaces the cache file by default; on Linux it works beside the cache file. It has no effect with the per-shader cache files of `SHARPEMU_VK_PIPELINE_CACHE_SHARDS`. | `VulkanVideoPresenter.ShaderPrewarm.cs`, `ShaderPrewarmList.cs`, `ShaderProgramCache.cs` |
| `SHARPEMU_SKIP_ALL_COMPUTE` | `1` | Set to `1` to make the Metal presenter skip all compute dispatches. The Vulkan presenter reads the value but does not use it. The default is off. | `MetalVideoPresenter.Compute.cs`, `VulkanVideoPresenter.cs` |
| `SHARPEMU_SKIP_TALL_COMPUTE_Z` | number | Effect not clear from the code. The Vulkan presenter reads the number into a field for a minimum Z group count. No code uses that field. The default is 0. | `VulkanVideoPresenter.cs` |
| `SHARPEMU_SRT_NATIVE` | `0` | `0` turns off the compiled shader resource (SRT) evaluator. By default, the resource plan of each shader is compiled once to .NET code (`DynamicMethod`) on a low-priority background thread named `SRT compiler`; until it is ready, the plan is evaluated by the interpreter, so the first use of a new shader does not wait for the compilation. With `0`, every plan always uses the interpreter. The compiled path is also off when the runtime does not support dynamic code, for example in a Native AOT build. | `CompiledResourceEvaluator.cs`, `ShaderResourcePlan.cs`, `RuntimeValueEvaluator.cs` |
| `SHARPEMU_SUSPEND_POINTS_IN_FLIGHT` | `0` | `0` makes `sceAgcSuspendPoint` wait until the frame boundary it queued is processed. The default lets one boundary wait behind the graphics commands, so the guest CPU can run one frame ahead. A wait that follows its own queue's store to the same label still sees that value when the title rewrites the label for the next frame (Dead Cells). | `CommandStreamQueue.cs`, `GpuCommandInterpreter.cs` |
| `SHARPEMU_VK_DEBUG_LABELS` | `1` | Turns on Vulkan object names and command labels for capture tools. `SHARPEMU_VK_VALIDATION=1` also turns them on. Default is off because the labels add overhead to each draw. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_VK_DEVICE` | text (part of a device name) | Selects the Vulkan device whose name contains this text. The comparison ignores case. When unset, the presenter gives a score to each device and prefers a discrete GPU. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanVideoPresenter.cs` |
| `SHARPEMU_VK_PIPELINE_CACHE_PATH` | path | Sets the path of the Vulkan pipeline cache file. The path can contain environment variables. Default is `user/pipeline_cache/<title id>/` below the application directory. The shader prewarm list goes to the same directory. With the per-shader cache, the files go to a `.shards` directory next to this path. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanPipelineCacheStorage.cs` |
| `SHARPEMU_VK_PIPELINE_CACHE` | `0`, `1` or `file` | Selects the Vulkan pipeline cache file, which keeps the driver's compiled pipelines. `1` or `file` saves and loads one cache file. `0` keeps the cache in memory only on every system. The default is memory only on Windows: the GPU driver keeps its own shader cache on disk, and the shader prewarm list (`SHARPEMU_SHADER_PREWARM`) rebuilds it after an update. Linux keeps a cache file by default, and macOS keeps per-shader files. | `VulkanVideoPresenter.Device.Setup.cs` |
| `SHARPEMU_VK_PIPELINE_CACHE_SHARDS` | `1` or `0` | `1` keeps one Vulkan pipeline cache file for each compute shader and for each vertex and pixel shader pair, in `vulkan-pipeline-cache.bin.shards`, also on Windows. `0` keeps the whole cache in one file. The default is `1` on macOS and `0` on other systems: NVIDIA adds about 2 MB of driver data to every cache file, so a file for each shader uses much more disk. A shard file loads when its pipeline is first created, and a save writes only the files that got new pipelines. The shard mode removes the old single file when `SHARPEMU_VK_PIPELINE_CACHE_PATH` is not set. A cache file that grows over 256 MiB is emptied, so the next launch starts it again. | `VulkanVideoPresenter.Device.Setup.cs`, `VulkanVideoPresenter.PipelineCacheShards.cs` |
| `SHARPEMU_VK_PIPELINE_CACHE_LEGACY_SHARDS` | `1` | `1` names the per-shader cache files by the guest shader hash. The default names them by a hash of the translated SPIR-V, so a translator change does not load records that no longer match. | `VulkanVideoPresenter.PipelineCacheShards.cs` |
## Metal
These variables apply to the Metal backend.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_GPU_DETILE` | `0` | The Metal backend decodes supported tiled texture data on the GPU by default. Set `0` to decode all tiled textures on the CPU. | `MetalTextureSnapshots.cs`, `GuestGpuTypes.cs` |
| `SHARPEMU_LOG_GPU_DETILE` | `1` | Writes each texture tile mode and each GPU detile decision one time to stderr. It applies to the Metal texture path only. The emulator reads the value one time at start. | `MetalTextureSnapshots.cs` |
| `SHARPEMU_METAL_CAP_DRAWABLE` | `1` | Limits the long edge of the Metal drawable to 1920 pixels and keeps the aspect ratio. The default is off, so the drawable uses the full window pixel size. | `MetalVideoPresenter.cs` |
| `SHARPEMU_METAL_DBG` | `solid` or `uv` | Replaces the Metal present fragment shader with a test shader. `solid` shows a green frame. `uv` shows the value of vertex attribute 0. All other values use the normal present shader. | `MetalVideoPresenter.cs` |
| `SHARPEMU_METAL_FULL_DRAWABLE` | `1` | Cancels `SHARPEMU_METAL_CAP_DRAWABLE=1`, so the Metal drawable uses the full window pixel size. It has no effect when the cap is off. | `MetalVideoPresenter.cs` |
| `SHARPEMU_NO_TEXTURE_SKIP` | `1` | Stops the reuse of cached texture snapshots in the Metal backend, so each texture is copied again. Use it to make sure that a snapshot is not stale. The default is off. | `MetalTextureSnapshots.cs` |
| `SHARPEMU_REUSE_GUEST_TEXTURE_SNAPSHOTS` | `0` | The default is on. The Metal backend uses a guest texture snapshot again when the snapshot key is the same. Set to `0` to make a new snapshot each time. | `MetalTextureSnapshots.cs` |
| `SHARPEMU_TEXTURE_DUMP_DIR` | path | Set a directory to make the Metal backend write raw texture source bytes to `.bin` files. The file name contains the address, size, pitch, format and tile mode. The limit is 200 files. | `MetalTextureSnapshots.cs` |
| `SHARPEMU_TEXTURE_LINEAR_DUMP_DIR` | path | Set a directory to make the Metal backend write the linear texture bytes to `.linear.bin` files. The file name contains the address, size, pitch, format and tile mode. The limit is 200 files. | `MetalTextureSnapshots.cs` |
| `SHARPEMU_TRACE_STORAGE_IMAGE_INIT_ADDRESS` | hex address | The Metal backend writes an `agc.storage_initial_data` trace line when it makes the texture at this guest address. The line shows the upload state and the read result of the initial data. | `MetalTextureSnapshots.cs` |
## Audio and video
These variables apply to audio output and video playback.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_ALSA_DEVICE` | text (ALSA device name) | Sets the ALSA playback device on Linux. The default is `default`. | `PosixAlsaAudioStream.cs` |
| `SHARPEMU_AUDIO_LATENCY_MS` | number (milliseconds) | Sets the target depth of the SDL audio queue, which is the playback latency. The default is 60. Values of zero or less use the default. | `SdlHostAudio.cs` |
| `SHARPEMU_BINK_MODE` | text (`native`, `ffmpeg`, `dummy`, `skip`, `guest`) | Selects how the emulator handles Bink movies. `native` or `ffmpeg` decodes on the host and is the default. `dummy` shows a placeholder frame, `skip` skips the movie, and `guest` lets the guest decode. | `HostMovieBridge.cs` |
| `SHARPEMU_LOG_MOVIE_SYNC` | `1` | Records the difference between the movie clock and the guest audio position during movie playback. The emulator reads the value one time at start. | `MediaFramePlayback.cs` |
| `SHARPEMU_MOVIE_CLOCK` | `wall` | Sets the time base for host-decoded movie playback. By default, playback follows the guest audio clock and uses the wall clock when no guest audio flows. `wall` makes playback always use the wall clock. | `MediaFramePlayback.cs` |
| `SHARPEMU_TRACE_AVPLAYER_IMAGES` | `1` | Set to `1` to write `[AVPLAYER][TRACE]` lines for video buffer addresses and the first 16 video frame payloads. It also marks the video buffer ranges for image traces in other components. The default is off. | `AvPlayerExports.cs` |
## Input
These variables apply to the controller and input libraries.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_AUTO_CROSS` | list (seconds, for example `40,52,64`) | Presses the Cross button for 0.4 seconds at each time in the list. The times are seconds from process start. The default is no automatic press. | `PadExports.cs` |
| `SHARPEMU_BTHID_CB_FAIL` | text (`nf` or `ni`) | Makes `sceBluetoothHidRegisterCallback` return an error. `nf` returns NOT_FOUND and `ni` returns NOT_IMPLEMENTED. The default returns the usual result. | `BluetoothHidExports.cs` |
| `SHARPEMU_BTHID_EVENT_CODE` | number (decimal or `0x` hex) | Sets the event code of the synthetic Bluetooth HID callback event. The default is 0. It has an effect only with `SHARPEMU_BTHID_FIRE_CALLBACK=1`. | `BluetoothHidExports.cs` |
| `SHARPEMU_BTHID_EVENT_SIZE` | number (bytes, decimal or `0x` hex) | Sets the size of the zeroed event structure for the synthetic Bluetooth HID callback. The default is 256. It has an effect only with `SHARPEMU_BTHID_FIRE_CALLBACK=1`. | `BluetoothHidExports.cs` |
| `SHARPEMU_BTHID_FIRE_CALLBACK` | `1` | Calls the registered Bluetooth HID guest callback one time with a zeroed event structure. The call occurs at the first `sceBluetoothHidRegisterDevice`. The default is off. | `BluetoothHidExports.cs` |
## Memory and CPU
These variables apply to guest memory and guest code execution.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_DISABLE_GUEST_ALLOCATOR_HOLE_RECOVERY` | `1` | Stops the Windows fault recovery for a guest allocator that reads an empty pool node. The default recovers the fault and continues at the allocator fallback code. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_DISABLE_MITIGATION_RELAUNCH` | `1` | Stops the Windows relaunch of the emulator as a child process with Control Flow Guard and CET shadow stacks off. The default starts the child process. | `Program.cs`, `SelfLoader.cs` |
| `SHARPEMU_DISABLE_NATIVE_GUEST_WORKERS` | `1` | Stops the use of native worker threads for guest thread execution on Windows. The default uses the native workers. | `DirectExecutionBackend.NativeWorker.cs` |
| `SHARPEMU_DISABLE_POSIX_SIGNALS` | `1` | Does not install the POSIX signal handlers on Linux and macOS. The emulator then cannot recover guest faults. | `DirectExecutionBackend.PosixSignals.cs` |
| `SHARPEMU_DISABLE_RAW_HANDLER` | `1` | Does not install the raw vectored exception handler on Windows. On POSIX hosts it stops the raw sentinel recovery. The default keeps the two functions on. | `DirectExecutionBackend.Exceptions.cs`, `DirectExecutionBackend.PosixSignals.cs` |
| `SHARPEMU_IGNORE_INT41` | `0` or `false` | The fault handler skips guest `int 0x41` instructions and continues at the next instruction by default. Set `0` or `false` to stop this recovery. The emulator shows a warning for the first 16 traps and then for each 65536th trap. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_IGNORE_STACK_CHK` | `1` | Recovers from a guest stack-protector failure call (NID `Ou3iL1abvng`) when a UD2 instruction follows the call. The emulator sets the return address to the function epilogue 20 bytes before. It does this only when the code bytes match the known pattern. | `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LAZY_RESERVE_PRIME_MB` | number | Sets the number of megabytes that the emulator commits at the start of each reserve-only memory region. The default is 64 and the maximum is 4096. The value 0 turns the initial commit off. | `PhysicalVirtualMemory.cs` |
| `SHARPEMU_NATIVE_WORKER_MAX_CONCURRENT` | number | Sets the maximum number of native guest worker runs that execute at the same time. The default is 2. The emulator limits the value to the range 1 to 64. | `DirectExecutionBackend.NativeWorker.cs`, `DirectExecutionBackend.cs` |
| `SHARPEMU_GUEST_AFFINITY` | `1` | Applies the guest thread affinity to the host threads. The default leaves host threads unpinned, because pinning let spinning guest workers starve the render thread. | `DirectExecutionBackend.cs` |
| `SHARPEMU_RENDER_CORE` | `0` | `0` turns off the render core reservation. On Windows hosts with 8 to 64 logical processors, the default gives the render thread the last physical core of the fastest efficiency class with a high priority, and keeps guest threads off that core. | `HostLaneReservation.cs`, `VulkanVideoPresenter.Present.cs`, `DirectExecutionBackend.cs` |
| `SHARPEMU_RESERVED_HOST_LANES` | number | Set the number of host logical processors that guest thread affinity does not use. The emulator keeps these processors for its own threads. The default is three eighths of the processor count, with a minimum of 2. It has an effect only with `SHARPEMU_GUEST_AFFINITY=1`. | `DirectExecutionBackend.cs` |
| `SHARPEMU_TSC_FREQ_HZ` | number (Hz, minimum 1000000) | Sets the TSC frequency that the kernel library reports to the guest. When unset, the emulator uses the calibrated RDTSC frequency, then the CPUID frequency, then the host stopwatch frequency. | `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_WATCH_BULK_DEST_HI` | hex number | Limits the `SHARPEMU_WATCH_BULK_TORN` scan to writes whose upper 32 address bits equal this value. When unset, the scan uses the direct-memory address band. | `GuestWriteWatch.cs` |
| `SHARPEMU_WATCH_BULK_TORN` | `1` | Examines each aligned 8-byte value in managed bulk writes to guest memory. It reports torn values and shifted pointer values with a stack trace. It writes a maximum of 64 reports for each kind. | `GuestWriteWatch.cs` |
| `SHARPEMU_WATCH_POOL_HEADER` | `1` | Monitors the header slot at offset 0x40 of each pool mapping with size 0x10000 and protection 0xF2. It reports each managed write to a monitored slot with a stack trace. It monitors a maximum of 64 slots. | `GuestWriteWatch.cs` |
| `SHARPEMU_WATCH_VALUE1` | `1` | Reports managed writes of 1 to 8 bytes that have the value 1 and go to the direct-memory address band. Each report has a stack trace. It writes a maximum of 128 reports. | `GuestWriteWatch.cs` |
| `SHARPEMU_WATCH_VALUE_PATTERN` | `1` | Reports each managed 8-byte write whose low half is 1 and whose high half is between 1 and 0xFFFF. Each report has a stack trace. | `GuestWriteWatch.cs` |
| `SHARPEMU_WATCH_WRITE` | hex address | Monitors the 8 bytes at this guest address. It reports each managed write that touches these bytes with the address, the length, the first value, and a stack trace. Default is off. | `GuestWriteWatch.cs` |
## Loader
These variables apply to the program loader and the module system.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_AMPR_INDEX_CACHE` | path (directory) | Sets the directory for the AMPR file index cache. The default is `SharpEmu/ampr-index` in the local application data folder. | `AmprFileRegistry.cs` |
| `SHARPEMU_AMPR_REINDEX` | `1` | Ignores the AMPR index cache on disk and scans the application file tree again. Use it after manual changes to the application files. | `AmprFileRegistry.cs` |
| `SHARPEMU_APP0_DIR` | path (directory) | Sets the host directory for the guest `/app0` mount. The default is the directory of the loaded executable. The directory name also identifies the per-application data folders. | `SharpEmuRuntime.cs`, `KernelMemoryCompatExports.cs`, `AppContentExports.cs` |
| `SHARPEMU_DEVLOG_APP_DIR` | path (directory) | Sets the host directory for the guest devlog application path. The default is `user/game_logs/<title id>/devlog/app` below the emulator directory. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_DISABLE_RED_ZONE_PATCH` | `1` | Stops the load-time patch that protects the guest stack red zone on Windows and macOS. The default applies the patch. The vector store split for Rosetta stays on when it is necessary. | `GuestRedZonePatcher.cs` |
| `SHARPEMU_DOWNLOAD0_DIR` | path (directory) | Sets the host directory for the guest `/download0` mount. The default is a `download0` folder in the per-application writable directory. The emulator sets the variable to the default when it is not set. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_GUEST_ARGS` | text | Adds arguments to the guest process entry frame after the image name. White space divides the arguments. The emulator uses only the first two. | `CpuDispatcher.cs` |
| `SHARPEMU_HOSTAPP_DIR` | path | Sets the host directory for the guest `/hostapp` mount. The default is `user/game_logs/<title id>/hostapp` below the emulator directory. The emulator makes the directory if it is absent. | `KernelMemoryCompatExports.cs`, `KernelGameLogPathTests.cs` |
| `SHARPEMU_LLE_LIBC_ALL` | `1` | Sends all libc imports to the guest libc module and not to the HLE handlers. `SHARPEMU_DISABLE_LLE_LIBC=1` and `SHARPEMU_LLE_LIBC_SAFE_ONLY=off` have priority. Default is off. | `DirectExecutionBackend.cs` |
| `SHARPEMU_LLE_LIBC_SAFE_ONLY` | `1`, `0`, `off`, `false` or `none` | Controls which libc imports use the guest libc module. Unset or `1` uses guest code only for exports on the safe list. `0` uses guest code for all libc exports, and `off`, `false` or `none` uses HLE for all of them. | `DirectExecutionBackend.cs` |
| `SHARPEMU_PRELOAD_ALL_SCE_MODULES` | `1` | Set to `1` to preload all system modules. When unset, the runtime does not preload the modules in its internal skip list. | `SharpEmuRuntime.cs` |
## Debugger
These variables apply to the debug server.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_SENTINEL_PROBE` | `1` | Set to `1` to call the native entry stub with the unresolved sentinel address 0xFFFE before the guest entry. The emulator writes a log line before and after the call. The default is off. | `DirectExecutionBackend.cs`, `Gen5NativeReturnSmokeTests.cs` |
## Log channels
Each variable adds one group of messages to the log. The default is off.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_LOG_ACM` | `1` | Writes `acm.` trace lines from the Acm library handlers to stderr. Default is off. | `AcmExports.cs` |
| `SHARPEMU_LOG_AGC_SHADER` | `1` | Writes only the AGC shader trace lines and the Vulkan shader trace lines. `SHARPEMU_LOG_AGC=1` also turns this on. Default is off. | `AgcExports.cs`, `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_LOG_AGC` | `1` | Starts the full AGC trace: command submissions, shader creation, draws and dispatches. It also turns on the AGC shader trace and the Vulkan shader trace. Default is off. | `AgcExports.cs`, `RenderExecutor.Trace.cs`, `AgcExports.ShaderState.cs` |
| `SHARPEMU_LOG_AJM` | `1` | Writes `ajm.` trace lines to stderr: context initialization, codec registration, split buffer descriptors and ATRAC9 decode state. Default is off. | `AjmExports.cs`, `Atrac9DecodeState.cs` |
| `SHARPEMU_LOG_ALLOC_IMPORTS` | `1` | Writes more trace lines for `sceKernelGetGPI` (the returned mask) and for the PRT aperture set call (the raw register values). Default is off. | `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_LOG_ALL_IMPORTS` | `1` | Writes a debug line for each import stub during setup. The lines show if the import uses a direct bridge, a trampoline, HLE or a runtime symbol. Default is off. | `DirectExecutionBackend.cs` |
| `SHARPEMU_LOG_AMPR_READS` | `1` | Writes only the AMPR file read trace lines and the pak directory tracker lines. `SHARPEMU_LOG_AMPR=1` also turns this on. Default is off. | `AmprExports.cs`, `PakDirectoryTracker.cs` |
| `SHARPEMU_LOG_AMPR` | `1` | Writes trace lines for AMPR command buffers, APR submissions and waits, and the pak directory tracker. It also turns on the AMPR read trace. Default is off. | `AmprExports.cs`, `KernelAprCompatExports.cs`, `PakDirectoryTracker.cs` |
| `SHARPEMU_LOG_APP_CONTENT` | `1` | Writes an `app_content` trace line to stderr for each AppContent call. The default is off. | `AppContentExports.cs` |
| `SHARPEMU_LOG_AUDIO_OUT2` | `1` | Writes an `audio_out2` trace line to stderr for each AudioOut2 operation. The default is off. | `AudioOut2Exports.cs` |
| `SHARPEMU_LOG_AUDIO_OUT` | `1` | Records `sceAudioOutOutput` calls and the peak amplitude of the submitted samples. It writes the first 8 calls and then every 200th call. The emulator reads the value one time at start. | `AudioOutExports.cs` |
| `SHARPEMU_LOG_BOOTSTRAP` | `1` | Writes a trace line for each call through the bootstrap bridge import. The line shows the handle, the symbol name and the output address. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_CONTEXT` | `1` | Writes `guest_context` trace lines to stderr. Each import dispatch shows the NID, the return address, the managed thread, the guest thread and the fiber. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_DATA_REBIND` | `1` | Writes one `[RUNTIME]` line for each imported data relocation. The line shows if the relocation is rebound, unresolved or write-failed. | `SharpEmuRuntime.cs` |
| `SHARPEMU_LOG_DISCMAP` | `1` | Writes a trace line for each DiscMap call. The line shows the export name, the path, the offset and the size. | `DiscMapExports.cs` |
| `SHARPEMU_LOG_DISCORD` | `1` | Writes `[DISCORD]` messages from the rich presence client of the GUI to stderr. The default is off. | `DiscordRichPresence.cs` |
| `SHARPEMU_LOG_DLSYM` | `1` | Writes a trace line for each successful `sceKernelDlsym` call. The line shows the module handle, the symbol name and the resolved address. | `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_EQUEUE` | `1` | Writes trace lines for kernel event queue operations. The emulator reads the value one time at start. | `KernelEventQueueCompatExports.cs` |
| `SHARPEMU_LOG_EVENT_FLAG` | `1` | Writes trace lines for kernel event flag operations. The emulator reads the value one time at start. | `KernelEventFlagCompatExports.cs` |
| `SHARPEMU_LOG_EXPECTED_IMPORT_RESULTS` | `1` | Shows import error results that the emulator knows as usual, for example file-not-found probes, timeouts and busy trylocks. It shows the first 8 results for each NID and result, and then every 10000th. The default hides these results. | `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_FIBER` | `1` | Writes `fiber` trace lines for the Fiber library calls. It also writes a line for each fiber context transfer in the CPU backend. | `FiberExports.cs`, `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_FILE` | path | Writes all log entries to this file with timestamps and appends to an existing file. The file receives every log level. `SHARPEMU_LOG_LEVEL` then limits only the console output. | `SharpEmuLog.cs` |
| `SHARPEMU_LOG_FMOD` | `1` | Writes a trace line for the FMOD `system_set_output` compatibility call. The line shows the system address, the output type and the call count. | `FmodCompatExports.cs` |
| `SHARPEMU_LOG_GUARDS` | `1` | Writes a trace line for each C++ static guard operation. The line shows the guard address, the guard word, the state flags and the owner thread. | `CxxAbiExports.cs` |
| `SHARPEMU_LOG_GUEST_EXCEPTIONS` | `1` | Writes `guest_exception` trace lines when the emulator raises and delivers a guest exception. The lines show the target thread and the exception type. | `DirectExecutionBackend.cs`, `KernelExceptionCompatExports.cs` |
| `SHARPEMU_LOG_GUEST_THREADS` | `1` | Writes guest thread scheduler messages to stderr. The messages show thread state changes, yields, wakes, exits, nested callbacks and affinity failures. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs`, `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_LOG_GUEST_THREAD_SNAPSHOTS` | `1` | Writes one snapshot line for each guest thread every second. The line shows the state, the import count, the last NID, the last return address and the block reason. The ready-dispatch thread then wakes every second. | `DirectExecutionBackend.cs` |
| `SHARPEMU_LOG_HTTP2` | `1` | Writes an `http2` trace line for each Http2 library call. The line shows the operation, the id and four arguments. | `Http2Exports.cs` |
| `SHARPEMU_LOG_HTTP` | `1` | Writes an `http` trace line for each Http library call. The line shows the operation, the id and four arguments. | `HttpExports.cs` |
| `SHARPEMU_LOG_IO_FILTER` | text | Limits the `SHARPEMU_LOG_IO` output to paths that contain this text. The comparison ignores case. When unset, the emulator shows all paths. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_JSON` | `1` | Writes a `json` trace line for each Json library call. Text values show a maximum of 128 characters. | `JsonExports.cs` |
| `SHARPEMU_LOG_LAZY_COMMIT` | `1` | Writes a trace line for every lazy memory commit fault. The default shows only the first 16 faults and then every 256th fault. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_LEVEL` | text | Sets the minimum log level. The values are `Trace`, `Debug`, `Info`, `Warning` or `Warn`, `Error`, `Critical` or `Fatal`, and `None`. The default is `Info`, and the comparison ignores case. | `SharpEmuLog.cs` |
| `SHARPEMU_LOG_LIBC_ALLOC` | `1` | Writes a trace line for each guest libc allocation call. The line shows the size, the alignment, the result address and the return address of the caller. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_NET` | `1` | Writes `net` trace lines for Net library calls and debug lines for kernel socket operations. It also writes each payload that the guest sends on a connected socket to stdout as text. | `NetExports.cs`, `KernelSocketCompatExports.cs` |
| `SHARPEMU_LOG_NGS2` | `1` | Writes trace lines for Ngs2 audio library calls. The default is off. | `Ngs2Exports.cs` |
| `SHARPEMU_LOG_NO_COLOR` | `1` | Stops the use of colors in the console log. The values `1`, `true`, `yes` and `on` are accepted. The console log has no colors when the output is redirected. | `SharpEmuLog.cs` |
| `SHARPEMU_LOG_NP_WEB_API2` | `1` | Writes an `npwebapi2` trace line for each NpWebApi2 call. The line shows the operation, the id, the first argument and the initialized state. | `NpWebApi2Exports.cs` |
| `SHARPEMU_LOG_OPEN` | `1` | Writes trace lines for guest file open operations to stderr. The default is off. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_PLAYGO` | `1` | Writes `playgo` trace lines for PlayGo calls. For single-entry locus queries it shows the first 32 calls and then every 1000th call. | `PlayGoExports.cs` |
| `SHARPEMU_LOG_POINTER_WINDOW_SIZE` | hex number | Sets the byte size of each memory window that `SHARPEMU_LOG_POINTER_WINDOWS` shows after a guest fault. The value is hexadecimal, with or without `0x`. The default is 0x80. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_POSIX_SIGNALS` | `1` | Writes a trace line for every POSIX signal that the fault handler receives. When unset, the log shows only the first 16 signals and then every 1024th signal. | `DirectExecutionBackend.PosixSignals.cs` |
| `SHARPEMU_LOG_PROC_PARAM_PTRS` | `1` | Shows the pointer fields in the process parameter block. This dump is part of the `SHARPEMU_LOG_PROC_PARAM` trace, so set that variable also. | `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_LOG_PROC_PARAM` | `1` | Shows the address and the contents of the process parameter block of the guest program. The default is off. | `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_LOG_PS5_USER_SLOTS` | `1` | Adds four 0x60-byte memory dumps to the IL2CPP exception diagnostic. The dumps start at the fixed guest address 0x801A73110 and have a step of 0x51C8 bytes. | `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_PSML` | `1` | Writes `psml.` trace lines for the PSML library calls to stderr. The default is off. | `PsmlExports.cs` |
| `SHARPEMU_LOG_PTHREADS` | `1` | Writes trace lines for the pthread calls of the guest. It also starts the condition variable trace of `SHARPEMU_LOG_PTHREAD_CONDS`. The default is off. | `KernelPthreadCompatExports.cs`, `KernelExports.cs` |
| `SHARPEMU_LOG_PTHREAD_CONDS` | `1` | Writes a `pthread_cond_` trace line for each condition variable operation. The line shows the waiter count, the signal epoch, and the result. | `KernelPthreadCompatExports.cs` |
| `SHARPEMU_LOG_PTHREAD_FASTPATH` | `1` | Writes trace lines for the mutex fast path. It shows the first 16 fast-path unlocks with the mutex object words. It shows one busy result for each mutex address. | `KernelPthreadCompatExports.cs` |
| `SHARPEMU_LOG_PTHREAD_MUTEX_FILTER` | list of hex addresses | Limits the mutex trace to the given mutex addresses. Use a comma, a semicolon, or a space between the hexadecimal addresses. The listed mutexes are in the trace even if `SHARPEMU_LOG_PTHREADS` is not set. | `KernelPthreadCompatExports.cs` |
| `SHARPEMU_LOG_REFSCAN_ADDRS` | list of hex addresses | After a guest fault, scans executable guest memory from 0x800000000 to 0x810000000 for instructions that refer to the given addresses. It shows a maximum of 24 hits for each address. Use a comma between the hexadecimal addresses. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_REGISTER_WINDOWS` | `1` | After a guest fault, shows a 0x80-byte memory window at each general register value that is 0x10000 or more. The default is off. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_SAVEDATA` | `1` | Writes `savedata.` and `save_data_dialog.` trace lines for the save data calls to stderr. The default is off. | `SaveDataExports.cs`, `SaveDataDialogExports.cs` |
| `SHARPEMU_LOG_SEMA` | `1` | Writes trace lines for the kernel semaphore operations. The emulator reads the value one time at start. The default is off. | `KernelSemaphoreCompatExports.cs` |
| `SHARPEMU_LOG_SHARE` | `1` | Writes `share.` trace lines for the Share library calls to stderr. The default is off. | `ShareExports.cs` |
| `SHARPEMU_LOG_SSL` | `1` | Writes an `ssl.` trace line for each SSL library call. The line shows the operation, the identifier, and the first argument. | `SslExports.cs` |
| `SHARPEMU_LOG_STACK_CHK` | `1` | Writes diagnostic lines when the guest calls the stack check failure import. The lines show the return address, the guard registers, and the stack slots from rbp-0x10 to rbp-0x80. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_STDIO` | `1` | Writes trace lines for the libc stdio calls of the guest. The emulator reads the value one time at start. The default is off. | `LibcStdioExports.cs` |
| `SHARPEMU_LOG_STRLEN_BURSTS` | `1` | Writes a warning when the guest calls `strlen` 24 times in sequence. The warning shows the return address and the last five different imports before the burst. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_STRLEN` | `1` | Keeps `strlen` calls in the import trace. When unset, the import trace does not show `strlen` calls. It also starts the burst detection of `SHARPEMU_LOG_STRLEN_BURSTS`. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_THREAD_MODE` | `1` | Writes `[THREADMODE]` lines when a native guest worker thread is created, starts a guest run, and stops a guest run. Each line shows the host thread identifiers. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.NativeWorker.cs` |
| `SHARPEMU_LOG_USER_SERVICE` | `1` | Writes trace lines for the user service library calls. Each line includes the guest return address of the call. | `UserServiceExports.cs` |
| `SHARPEMU_LOG_USLEEP` | `1` | Writes trace lines for `usleep` calls: the first 32 calls and then every 10000th call. It also stops the native inline `usleep` stub and the leaf import path, so each call goes through the traced handler. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs`, `KernelRuntimeCompatExports.cs` |
| `SHARPEMU_LOG_VIDEOOUT_FPS` | `1` | Writes periodic frame rate lines with flip, present, draw, pipeline, and buffer pool counts. `SHARPEMU_PROFILE_PERFORMANCE=1` starts the same output. | `VideoOutExports.cs` |
| `SHARPEMU_LOG_VIDEOOUT` | `1` | Writes `videoout.` trace lines for the video output calls, such as flip events and frame dumps. The default is off. | `VideoOutExports.cs` |
| `SHARPEMU_LOG_VK_RESOURCES` | `1` | Writes a `vk.global_buffer` trace line one time for each new guest buffer that the Vulkan presenter binds. The line shows the base address and the size. | `VulkanVideoPresenter.Draws.Recording.cs`, `VulkanVideoPresenter.GuestBuffers.cs` |
| `SHARPEMU_LOG_VMEM` | `1` | Writes debug lines for guest virtual memory operations and `[HOSTMEM]` lines for host memory operations. The default is off. | `PhysicalVirtualMemory.cs`, `HostMemory.cs`, `PosixHostMemory.cs` |
| `SHARPEMU_LOG_WIDE_PRINTF_ARGS` | `1` | Writes trace lines for the arguments of printf format calls, such as string arguments. Use `SHARPEMU_LOG_WIDE_PRINTF_FILTER` to limit the output. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_WIDE_PRINTF_FILTER` | text | Limits the `SHARPEMU_LOG_WIDE_PRINTF_ARGS` trace to format strings that contain this text. The comparison is case-sensitive. When unset, the trace includes all format strings. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_WIDE_PRINTF` | `1` | Writes a trace line for each wide-character printf call. The line shows the first 160 characters of the format and of the result. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_LOG_WIDE` | `1` | Shows the first 32 bytes at the string address for each `wcslen` call. The default is off. | `KernelMemoryCompatExports.cs` |
| `SHARPEMU_PERIODIC_SNAPSHOT_SECONDS` | number (seconds) | Set a number of seconds to write the stall watchdog snapshot at that interval. The default is 0, which writes no periodic snapshot. The stall watchdog must be on. | `DirectExecutionBackend.cs` |
## Traces
Each variable records a detailed sequence of events. Traces can be large.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_LOG_IMPORT_FILTER` | text | Writes a trace line for each import that contains this text. The emulator compares the text with the library name, the export name and the NID, and ignores case. When unset, the filter selects no imports. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_IMPORT_FRAMES` | `1` | Adds the guest frame chain to each traced import. It applies only to imports that `SHARPEMU_LOG_ALL_IMPORTS` or `SHARPEMU_LOG_IMPORT_FILTER` selects. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_IMPORT_PERIODIC` | `1` | Starts the periodic import trace. It shows the early import bands and every 100000th import dispatch. The default is off because the stderr output decreases the speed. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_IMPORT_RECENT` | `1` | Writes the recent import history after each traced import. It applies only to imports that `SHARPEMU_LOG_ALL_IMPORTS` or `SHARPEMU_LOG_IMPORT_FILTER` selects. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_PROBE_IMPORT_RET_ADDRESS` | hex address | Set a guest return address. When an import call returns to this address, the emulator writes an `import-return-address-probe` line with the thread, NID, stack and frame values. The limit is 2048 lines. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_PROBE_IMPORT_RET` | text (NID or `*`) | Set an import NID, or `*` for all imports. For the first 8 applicable import calls, the emulator writes the bytes and the disassembly before the return address. The default is off. | `DirectExecutionBackend.cs`, `DirectExecutionBackend.Imports.cs`, `DirectExecutionBackend.Diagnostics.cs` |
| `SHARPEMU_RTC_PROBE_RANGE` | hex address range (`start-end`) | Set a range of guest addresses. When a caller of `sceRtcGetCurrentTick` returns into this range, the emulator writes 0x100 bytes of code near that return address. It does this one time only. | `RtcExports.cs` |
| `SHARPEMU_TRACE_AGC_EQ_ACCESSORS` | `1` | Set to `1` to write an `agc.eq_accessor` line for calls to the AGC event accessor functions. Each line shows the accessor, the event address, the result and the event bytes. After 64 lines, the emulator writes only at power-of-two counts. | `AgcExports.Events.cs` |
| `SHARPEMU_TRACE_DCC_WRITERS` | `1` | Writes a `[GPU][DCC_WRITER]` line with the shader program and a call stack for each GPU buffer write that overlaps DCC metadata. It also writes `[GPU][BOUNDED_FILL_REFUSED]` lines when a fill shader is not changed into a buffer fill. Each kind writes a maximum of 64 lines. | `DccWriterTrace.cs` |
| `SHARPEMU_TRACE_FOCUSED_CONTINUATION` | `1` | Set to `1` to write `focused_continuation` lines for guest thread continuations. The emulator writes a line only when the stack pointer is in the range 0x6FFFAC000000 to 0x6FFFAC200000. | `DirectExecutionBackend.cs` |
| `SHARPEMU_TRACE_FRAME_PACKETS` | `1` | Writes a `[FRAMEPKT]` line at a flip with the draw count and the dispatch count of the frame. It shows the first 8 flips, each 60th flip, and each flip that has no draws. Default is off. | `AgcExports.CommandStream.cs` |
| `SHARPEMU_TRACE_GPU_MEMORY_ADDRESS` | hex address or `auto` | Writes `[GPU][MEMORY_TRACE]` lines for GPU memory events that touch the page of the given address. The value `auto` selects the page of the first device-address fault. Default is off. | `GuestGpuMemoryHook.cs` |
| `SHARPEMU_TRACE_GPU_READS` | `1` | Writes a `[GPU][SYNC_READ]` line with the shader hash, the stage and the address when a shader resource read must wait for GPU-written memory. It writes one line for each shader and page, and a maximum of 64 lines. | `GpuReadTrace.cs`, `VulkanVideoPresenter.Pipelines.cs` |
| `SHARPEMU_TRACE_GUEST_IMAGES` | `1`, `present` or `every:N[@M]` | `1` and `present` write Vulkan present trace lines (`vk.present_taken`, `vk.present_dropped`, `vk.present_sample`) and up to 64 Metal `agc.texture_fallback` lines. The code reads the `every:N[@M]` form, but no code uses the result. Default is off. | `VulkanVideoPresenter.Draws.Recording.cs`, `VulkanVideoPresenter.Present.cs`, `MetalTextureSnapshots.cs` |
| `SHARPEMU_TRACE_GUEST_IMAGE_ADDRS` | list of hex addresses or `*` | The guest image write tracker writes `[WT][LIFETIME]` lines for each tracked range that contains a listed address. `*` selects all ranges. The Vulkan presenter also reads the list, but no code uses that filter. | `GuestImageWriteTracker.cs`, `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_GUEST_IMAGE_FORMAT` | text (Vulkan format name) | Effect not clear from the code. The Vulkan presenter keeps the name for an image filter that compares the image format, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_GUEST_IMAGE_HEIGHT` | number | Effect not clear from the code. The Vulkan presenter keeps the number for an image filter that compares the image height, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_GUEST_IMAGE_WIDTH` | number | Effect not clear from the code. The Vulkan presenter keeps the number for an image filter that compares the image width, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_GUEST_MEMORY_LIFETIME` | list of source names or `*` | The guest image write tracker writes `[WT][LIFETIME]` lines for each tracked range that has a listed source name. Use a comma or a semicolon between names. `*` selects all sources. | `GuestImageWriteTracker.cs` |
| `SHARPEMU_TRACE_GUEST_WRITES` | list of hex addresses or `*` | Effect not clear from the code. The Vulkan presenter reads the list in an address filter for guest image writes, but no code calls the filter. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_PACKED_EXPORT` | `1` | The SPIR-V translator writes `[AGC][PACKED-EXPORT]` and `[AGC][TITLE-IR]` lines for packed export analysis. It does this only for the shader program at address 0x0000000500781200. Default is off. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_ADDRS` | list of hex addresses or `*` | Writes a `vk.present_sample` trace line when the presenter shows a guest image at a listed address. It records the first presentation of each address unless `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_OCCURRENCE` is set. | `VulkanVideoPresenter.Draws.Recording.cs`, `VulkanVideoPresenter.RenderLoop.cs` |
| `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_OCCURRENCE` | number | Selects which presentation of an address in `SHARPEMU_TRACE_PRESENTED_GUEST_IMAGE_ADDRS` the presenter records. Default is 0, which selects the first presentation. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_TRACE_TITLE_INTERFACE` | `1` | The SPIR-V translator writes `[AGC][TITLE-INTERFACE]` lines with the export and interpolation instructions of a shader. It does this only for the programs at addresses 0x0000000500780000 and 0x0000000500781200. | `Gen5SpirvTranslator.cs` |
| `SHARPEMU_TRACE_WRITER_ADDRESS` | hex address | Writes a `[GPU][TARGET_WRITER]` line with the shader program and a call stack for each GPU buffer write that covers this guest address. It writes a maximum of 64 lines. Default is off. | `DccWriterTrace.cs` |
## Data dumps
Each variable writes data to files.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_DUMP_FAULT_STACK_WINDOW` | `1` | Writes the full stack window from RSP-0x300 to RSP+0x100 to the error output when a guest fault report occurs. The default writes only the short stack list. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_DUMP_FIXED_SOLID_FRAGMENT` | path | The Vulkan presenter reads the value into a field. No code uses the field, so the variable has no effect. | `VulkanVideoPresenter.Draws.Recording.cs` |
| `SHARPEMU_DUMP_SPIRV_ADDRESS` | hex address | Limits the shader dump of `SHARPEMU_DUMP_SPIRV=1` to the shader at this guest address. The default writes all shaders. | `ShaderProgramCache.cs` |
| `SHARPEMU_DUMP_VIDEOOUT` | `1` | Writes submitted VideoOut frame buffers as BMP files with a text metadata file. It writes a maximum of 8 frames and skips a frame that did not change. | `VideoOutExports.cs`, `VideoOutExports.FlipRequests.cs` |
| `SHARPEMU_GUEST_IMAGE_DUMP_CONTINUOUS` | `1` | Writes a swapchain dump for each presented frame, not only for the first one. It needs `SHARPEMU_GUEST_IMAGE_DUMP_DIR` and an active presented-image trace. Each dump waits for the GPU, so the frame rate decreases. | `VulkanVideoPresenter.Present.Frames.cs` |
| `SHARPEMU_GUEST_IMAGE_DUMP_DIR` | path | Sets the directory for raw swapchain dumps. The presenter writes `present-NNNN-WxH-format.bgra` files there when `SHARPEMU_TRACE_GUEST_IMAGES` is `1` or `present`. The presenter makes the directory if it is absent. | `VulkanVideoPresenter.Present.Frames.cs` |
| `SHARPEMU_LOG_DISASM_ADDRS` | list of hex addresses | Adds more addresses to the fault disassembly dump. Use a comma between the addresses. The emulator uses this list only when `SHARPEMU_LOG_DISASM` is `1`, and it shows 48 instructions for each address. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_DISASM` | `1` | Writes guest disassembly to stderr when a guest fault, an abort or an illegal instruction occurs. It shows the code before the fault address, before the stack return address and before three frame return addresses. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_LOG_IL2CPP_EXCEPTION` | `1` | Writes a diagnostic dump when the guest calls the unresolved import with NID `cfwBSQyr5Ys`. The dump shows the registers, the code pointers on the stack and the frame pointer chain. The emulator writes a maximum of 4 dumps. | `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_LOG_POINTER_WINDOWS` | list of hex addresses | Writes a memory window for each address when a guest access fault occurs. Use a comma between the addresses. The window size is 0x80 bytes unless `SHARPEMU_LOG_POINTER_WINDOW_SIZE` sets a different size. | `DirectExecutionBackend.Exceptions.cs` |
| `SHARPEMU_SHADER_SPIRV_DUMP_DIR` | path | Set a directory for shader dumps. When set, the Vulkan presenter writes each SPIR-V module to a numbered `.spv` file. The `SHARPEMU_DUMP_SPIRV=1` dumps also use this directory. Their default is `shader-dumps` next to the executable. | `VulkanVideoPresenter.Pipelines.cs`, `ShaderProgramCache.cs`, `VulkanVideoPresenter.Device.Setup.cs` |
| `SHARPEMU_SWAPCHAIN_DUMP_EVERY` | number (frames) | Set a number N to trace the presented swapchain image at each Nth present. This operates only when `SHARPEMU_TRACE_GUEST_IMAGES` is `1` or `present`. The default is 0, which traces only the first presented image. | `VulkanVideoPresenter.Present.Frames.cs`, `VulkanVideoPresenter.Draws.Recording.cs` |
## Performance measurement
Each variable records timings or counts.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_LOG_AUDIO_QUEUE` | `1` | Writes one `[PERF][AUDIO]` line per second for each SDL audio stream. The line shows the queue depth in milliseconds, the submit rate, the fill percentage and the drops. The emulator reads the value one time at start. | `SdlHostAudio.cs` |
| `SHARPEMU_PERF_HLE_NODICT` | `1` | Use with `SHARPEMU_PERF_HLE=1`. Set to `1` to stop the call count dictionary and the top call count report. The dispatch time report stays on. | `DirectExecutionBackend.Diagnostics.cs` |
| `SHARPEMU_PERF_HLE` | `1` | Set to `1` to measure the time of each HLE import dispatch. After each 500000 calls, the emulator writes `[PERF][HLE]` lines with the call counts and the cost of the top exports. The default is off. | `DirectExecutionBackend.Diagnostics.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_PERF_MEM` | `1` | Set to `1` to count the POSIX fault signals that the signal handler receives. The emulator writes a `[PERF][MEM] posix_faults` line after each 100000 signals. This has an effect only on POSIX hosts. | `DirectExecutionBackend.PosixSignals.cs` |
| `SHARPEMU_PROFILE_DCB_PARSE` | `1` | Set to `1` to measure the time to parse draw command buffers. The emulator writes `[PERF][DCB_PARSE]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `DcbParseProfile.cs` |
| `SHARPEMU_PROFILE_DCB_SUBMISSION` | `1` | Set to `1` to measure command buffer submission and geometry snapshot work. The emulator writes `[PERF][DCB_SUBMIT]`, `[PERF][VERTEX_SNAPSHOT]` and `[PERF][SNAPSHOT_PREPASS]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `DcbSubmissionProfile.cs` |
| `SHARPEMU_PROFILE_GPU_DRAWS` | `1` | Use with the render profile. Writes a GPU timestamp after each draw and dispatch, so that the `[PERF][GPU_INTERVAL]` lines give the GPU time of each pipeline. The default writes only two timestamps for each submission for the `[PERF][GPU_BUSY]` lines, because one timestamp for each draw can change the GPU time. | `VulkanCommandProfile.cs` |
| `SHARPEMU_PROFILE_GUEST_IMAGE_TRACKER` | `1` | Set to `1` to record counters for the guest image write tracker. The emulator writes `[PERF][GUEST_IMAGE_TRACKER]` and `[PERF][GUEST_IMAGE_TRACKER_RANGE]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `GuestImageWriteTracker.cs` |
| `SHARPEMU_PROFILE_GUEST_RIP_INTERVAL_MS` | number (milliseconds) | Set the time between samples of the guest code profiler. The value must be more than 0. The default is 2. | `DirectExecutionBackend.GuestSampler.cs` |
| `SHARPEMU_PROFILE_GUEST_RIP_REPORT_S` | number (seconds) | Set the time between reports of the guest code profiler. The value must be more than 0. The default is 15. | `DirectExecutionBackend.GuestSampler.cs` |
| `SHARPEMU_PROFILE_GUEST_RIP_THREAD` | text | Set a text filter for the guest code profiler. The profiler samples only guest threads whose name contains this text. The comparison ignores case. When unset, the profiler samples all guest threads. | `DirectExecutionBackend.GuestSampler.cs` |
| `SHARPEMU_PROFILE_GUEST_RIP` | `1` | Set to `1` to start a sample profiler for guest code. The profiler records the instruction address of each guest thread and writes `[PERF][GUEST]` reports. The default is off. | `DirectExecutionBackend.GuestSampler.cs`, `DirectExecutionBackend.Imports.cs` |
| `SHARPEMU_PROFILE_GUEST_SCHEDULER` | `1` | Set to `1` to record guest thread scheduler statistics. The emulator writes `[PERF][GUEST_SCHED]` lines. With `SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE=1`, it also records guest thread flow events. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `DirectExecutionBackend.GuestSchedulerProfile.cs`, `GuestThreadFlowProfile.cs` |
| `SHARPEMU_PROFILE_RENDER_REPORT_S` | number (seconds) | Set the time between render profile reports. The value can be a decimal number and must be more than 0. The default is 5. | `RenderPhaseProfile.cs` |
| `SHARPEMU_PROFILE_RENDER` | `1` | Set to `1` to measure the time of each render thread phase. The emulator writes `[PERF][RENDER]`, `[PERF][RENDER_MS]` and related lines. With `SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE=1`, it also records GPU memory access counters. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `RenderPhaseProfile.cs`, `GpuMemoryAccessProfile.cs` |
| `SHARPEMU_PROFILE_SYNC_ON_ADDRESS` | `1` | Set to `1` to start the detailed sync-on-address profile. The emulator also records the guest call stack for each wait and wake call. It writes `[PERF][SYNC_ADDR]` and `[PERF][SYNC_THREAD]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` starts the profile without the call stack. | `KernelSyncOnAddressProfile.cs`, `KernelSyncOnAddressCompatExports.cs` |
| `SHARPEMU_PROFILE_TEXTURE_PREPARATION` | `1` | Set to `1` to measure texture preparation work. The emulator writes `[PERF][TEXTURE_PREP]` lines. `SHARPEMU_PROFILE_PERFORMANCE=1` also starts this profile. | `TexturePreparationProfile.cs` |
## Internal
The emulator sets these variables for its own processes. Do not set them manually.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_MITIGATED_CHILD` | `1` | The Windows launcher sets this variable to 1 when it starts the child process with CFG and CET mitigations off. The child accepts the `--sharpemu-mitigated-child` argument only when the value is 1. Do not set it manually. | `Program.cs`, `EmulatorProcess.cs` |
## Tests and tools
Only the tests, the scripts or the build tools read these variables.
| Variable | Value | Function | Code that reads it |
| --- | --- | --- | --- |
| `SHARPEMU_BACKING_VIEW_FAULT_WORKER` | `1` | The backing view fault test sets this for its child process. The value `1` tells the child to do the fault sequence. Do not set it manually. | `BackingViewFaultTests.cs`, `GuestFaultWorker.cs` |
| `SHARPEMU_BUFFER_FAULT_WORKER` | `1` | The guest buffer fault test sets this for its child process. The value `1` tells the child to do the fault sequence. Do not set it manually. | `GuestBufferFaultTests.cs`, `GuestFaultWorker.cs` |
| `SHARPEMU_FAULT_WORKER_REPORT` | path | The fault test parent sets this to a temporary file path for its child process. The child writes its result (`completed` or `skipped`) to this file. When unset, the child writes the result to the error stream. | `GuestFaultWorker.cs` |
| `SHARPEMU_IMAGE_FAULT_WORKER` | `1` | The guest image fault test sets this for its child process. The value `1` tells the child to do the fault sequence. Do not set it manually. | `GuestImageFaultTests.cs`, `GuestFaultWorker.cs` |
| `SHARPEMU_NATIVE_RETURN_SMOKE_WORKER` | `1` | The native return smoke test sets this for its child process. The value `1` tells the child to run the synthetic guest entry. Do not set it manually. | `Gen5NativeReturnSmokeTests.cs` |
| `SHARPEMU_SOCKET_CONNECTION_TEST_WORKER` | `1` | The kernel socket connection test sets this for its child process. The value `1` tells the child to do the connect sequence. Do not set it manually. | `KernelSocketConnectionTests.cs` |
| `SHARPEMU_TEST_REFERENCE_SPIRV_DIR` | path | Sets the directory that contains the reference compiled shader files for the GPU image tests. The tests make sure that each file has the recorded SHA-256 hash. When unset, these tests skip the reference comparison. | `ImageTestHarness.cs` |
| `SHARPEMU_TEST_REQUIRE_DEVICE` | `1` | Makes a GPU test fail when a Vulkan or Metal device or a device feature is missing. Default is to skip the test. | `ImageTestHarness.cs`, `MetalRuntimeTests.cs` |
| `SHARPEMU_TEST_REQUIRE_REFERENCE_SPIRV` | `1` | Makes a test fail when a reference shader file, its directory, or a necessary device is missing. Default is to skip the test. | `ImageTestHarness.cs`, `GuestFaultWorker.cs` |
| `SHARPEMU_TEST_VK_VALIDATION` | `1` | Loads the Vulkan validation layer in the headless test device and prints the validation messages. Default is off. | `HeadlessVulkan.cs` |
| `SHARPEMU_THREAD_CONTEXT_TEST_WORKER` | `1` | The Windows thread context capture test sets this for its child process. The value `1` tells the child to do the capture sequence. Do not set it manually. | `WindowsThreadContextCaptureTests.cs`, `GuestFaultWorker.cs` |
| `SHARPEMU_UPDATE_GOLDENS` | `1` | Makes the Metal golden tests write the generated shader text to the golden files in the source tree. Default is to compare the generated text with the golden files. | `MslGoldenTests.cs` |
## Variables that the code reads but does not use
The code reads these variables into a field or a filter. No other code uses that field or filter.
Thus they have no effect, or only a part of their function operates. The rows above give the details.
- `SHARPEMU_DUMP_FIXED_SOLID_FRAGMENT`
- `SHARPEMU_ENABLE_CHUNKED_DRAWS`
- `SHARPEMU_FORCE_ATTRIBUTE_FRAGMENT`
- `SHARPEMU_FORCE_DEFAULT_RASTER_STATE`
- `SHARPEMU_FORCE_FULLSCREEN_PIPELINE`
- `SHARPEMU_FORCE_FULLSCREEN_VERTEX`
- `SHARPEMU_FORCE_SOLID_FRAGMENT`
- `SHARPEMU_FORCE_TITLE_DEFAULT_RASTER_STATE`
- `SHARPEMU_FORCE_TITLE_FULLSCREEN_VERTEX`
- `SHARPEMU_FORCE_TITLE_SOLID_FRAGMENT`
- `SHARPEMU_SKIP_TALL_COMPUTE_Z`
- `SHARPEMU_TRACE_GUEST_IMAGE_FORMAT`
- `SHARPEMU_TRACE_GUEST_IMAGE_HEIGHT`
- `SHARPEMU_TRACE_GUEST_IMAGE_WIDTH`
- `SHARPEMU_TRACE_GUEST_WRITES`

View File

@ -1,48 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
# Windows crash capture
In the launcher, open **Options > Logging** and enable **Crash dumps** before
you start a game. The same toggle is available in each game's Logging settings.
It sets `SHARPEMU_CRASH_CAPTURE` and is off by default.
For a diagnostic run on Windows x64, you can also set it from PowerShell:
```powershell
$env:SHARPEMU_CRASH_CAPTURE = '1'
.\SharpEmu.exe
```
Start a new launcher from that PowerShell window. The game process starts a
hidden helper before runtime initialization. No SDK or debugger install is
required. Look for `[CRASH][INFO] Crash capture ready` in the game log.
The helper writes a full dump beside the run log. Without a log path, it uses
`user/logs` beside the executable. Names include the game process ID:
- `<run>.crash-<pid>.dmp`: the memory dump.
- `<run>.crash-<pid>.dmp.capture.log`: capture status and errors.
Full dumps can use many GB of disk space and contain private process data.
Keep them local until you choose to share them. There is no automatic upload.
The helper uses Windows debugger events. It passes first-chance exceptions to
the application and captures only second-chance, unhandled exceptions. Normal
handled page faults do not create dumps. A normal exit or a forced process
termination does not create a dump either. The original crash is not suppressed.
This option changes debugger presence and can change timing. Do not use it for
FPS comparisons. Do not attach WinDbg, ProcDump, or another native debugger at
the same time. Capture failures are recorded when possible; no capture method
can cover every process termination. A failed write can leave an incomplete dump.
This is separate from the `DOTNET_Dbg*` settings. It does not change them.
To disable a GUI selection, turn off **Crash dumps** before the next game launch.
If you set the option in PowerShell, close the launcher and run:
```powershell
Remove-Item Env:SHARPEMU_CRASH_CAPTURE -ErrorAction SilentlyContinue
```

View File

@ -41,36 +41,21 @@ def write_pair(nid: str, export_name: str) -> None:
def lookup(args: argparse.Namespace) -> int:
values = [value.strip() for value in args.value.split(",") if value.strip()]
names = read_names(args.names)
name_set = set(names)
value = args.value.strip()
if NID_PATTERN.fullmatch(value):
for export_name in read_names(args.names):
if compute_nid(export_name) == value:
write_pair(value, export_name)
return 0
result = 0
print(f"NID not found in catalog: {value}", file=sys.stderr)
return 1
for value in values:
if NID_PATTERN.fullmatch(value):
found = False
for export_name in names:
if compute_nid(export_name) == value:
write_pair(value, export_name)
found = True
break
if not found:
print(f"NID not found in catalog: {value}", file=sys.stderr)
result = 1
continue
write_pair(compute_nid(value), value)
if value not in name_set:
print(
f"Warning: export name is not present in the catalog: {value}",
file=sys.stderr,
)
return result
names = set(read_names(args.names))
write_pair(compute_nid(value), value)
if value not in names:
print("Warning: export name is not present in the catalog.", file=sys.stderr)
return 0
def search(args: argparse.Namespace) -> int:
@ -147,9 +132,7 @@ def create_parser() -> argparse.ArgumentParser:
lookup_parser = subparsers.add_parser(
"lookup", help="resolve a NID or calculate the NID for an export name"
)
lookup_parser.add_argument(
"value", help="11-character NID or exact export name; comma-separated values are supported"
)
lookup_parser.add_argument("value", help="11-character NID or exact export name")
lookup_parser.set_defaults(handler=lookup)
search_parser = subparsers.add_parser(

View File

@ -133845,7 +133845,6 @@ sceKernelWriteRemapIntoPrtCommand
sceKernelWriteRemapWithGpuMaskIdCommand
sceKernelWriteSdkEventLog
sceKernelWriteSdkEventLogV
sceKernelWriteThrottlingStatus
sceKernelWriteUnmapCommand
sceKernelWriteUnmapToPrtCommand
sceKernelWriteWaitOnAddressCommand
@ -154457,6 +154456,3 @@ sceNpEAAccessInitialize
sceNpEAAccessTerminate
sceNpHasEAAccessSubscription
sceNpHasEAAccessSubscriptionAbortRequest
sceAgcAddPrimStateRegisters
sceVoiceQoSSetMode
sceVoiceQoSTerminate

View File

@ -32,6 +32,7 @@ internal static partial class Program
private const ulong PROCESS_CREATION_MITIGATION_POLICY_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_CET_USER_SHADOW_STACKS_ALWAYS_OFF = 0x00000002UL << 28;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_USER_CET_SET_CONTEXT_IP_VALIDATION_ALWAYS_OFF = 0x00000002UL << 32;
private const ulong PROCESS_CREATION_MITIGATION_POLICY2_XTENDED_CONTROL_FLOW_GUARD_ALWAYS_OFF = 0x00000002UL << 40;
private const int ATTACH_PARENT_PROCESS = -1;
private const int STD_INPUT_HANDLE = -10;
private const int STD_OUTPUT_HANDLE = -11;
@ -47,10 +48,6 @@ internal static partial class Program
{
ConfigureManagedPluginResolution();
if (args.Length > 0 && args[0] == SharpEmu.Core.Diagnostics.WindowsCrashCapture.HelperArgument)
return SharpEmu.Core.Diagnostics.WindowsCrashCapture.RunHelper(args);
RenderDocCapture.ApplyVulkanLoaderEnvironment();
SharpEmu.Libs.VideoOut.RenderDocCapture.Initialize();
try
@ -307,8 +304,6 @@ internal static partial class Program
Console.Error.WriteLine("[DEBUG] Creating runtime...");
SharpEmu.Core.Diagnostics.WindowsCrashCapture.StartIfEnabled(logFilePath);
try
{
using var runtime = SharpEmuRuntime.CreateDefault(runtimeOptions);
@ -323,16 +318,6 @@ internal static partial class Program
VideoOutExports.NotifyHostInterrupt();
};
Console.CancelKeyPress += cancelHandler;
// SIGTERM (launchd, process managers, `kill`) takes the same cooperative shutdown: the
// runtime's default exits with GPU work in flight, which leaves the host driver to reclaim
// resources the device still owns.
using var terminateRegistration = OperatingSystem.IsWindows()
? null
: PosixSignalRegistration.Create(PosixSignal.SIGTERM, context =>
{
context.Cancel = true;
VideoOutExports.NotifyHostInterrupt();
});
Console.Error.WriteLine($"[DEBUG] Running: {ebootPath}");
result = runtime.Run(ebootPath);
@ -535,15 +520,9 @@ internal static partial class Program
return false;
}
var commandLineArguments = Environment.GetCommandLineArgs();
var entryAssemblyPath = commandLineArguments.Length > 0 ? commandLineArguments[0] : null;
string[] childArguments =
Path.GetFileNameWithoutExtension(processPath).Equals("dotnet", StringComparison.OrdinalIgnoreCase) &&
!string.IsNullOrWhiteSpace(entryAssemblyPath)
? [entryAssemblyPath, MitigatedChildFlag, .. args]
: [MitigatedChildFlag, .. args];
string[] childArgs = [MitigatedChildFlag, .. args];
var commandLine = BuildCommandLine(processPath, childArguments);
var commandLine = BuildCommandLine(processPath, childArgs);
var startupInfoEx = new STARTUPINFOEX();
startupInfoEx.StartupInfo.cb = Marshal.SizeOf<STARTUPINFOEX>();
ConfigureInheritedStdHandles(ref startupInfoEx.StartupInfo);
@ -1010,7 +989,7 @@ internal static partial class Program
private static void PrintUsage()
{
Log.Info("Usage: SharpEmu.CLI [--strict] [--trace-imports[=N]] [--cpu-engine=<native>] [--console=<ps5>] [--console-language=<language>] [--log-level=<level>] [--log-file[=<path>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--overlay=<on|off>] [--overlay-mode=<full|minimal|titlebar>] [--overlay-corner=<topleft|topright|bottomright|bottomleft>] [--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>]] [--window-mode=<windowed|borderless|exclusive>] [--resolution=<WIDTHxHEIGHT>] [--display=<N>] [--refresh-rate=<HZ>] [--scaling=<fit|cover|stretch|integer>] [--vsync=<on|off>] [--hdr=<auto|on|off>] [--debug-server[=host:port]] <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("Debug server: --debug-server starts a live debug listener (default 127.0.0.1:5714); connect with SharpEmu.DebugClient.");
}
@ -1072,7 +1051,6 @@ internal static partial class Program
var strictDynlibResolution = false;
var importTraceLimit = 0;
var cpuEngine = CpuExecutionEngine.NativeOnly;
var systemLanguage = 1;
HostWindowMode? windowModeOverride = null;
HostScalingMode? scalingModeOverride = null;
int? windowWidthOverride = null;
@ -1081,9 +1059,6 @@ internal static partial class Program
int? refreshRateOverride = null;
bool? vsyncOverride = null;
HostHdrMode? hdrModeOverride = null;
bool? overlayEnabledOverride = null;
PerformanceOverlayMode? overlayModeOverride = null;
PerformanceOverlayCorner? overlayCornerOverride = null;
videoOptions = HostVideoOptions.Default;
logFilePath = null;
logLevel = SharpEmuLog.MinimumLevel;
@ -1091,26 +1066,6 @@ internal static partial class Program
for (var i = 0; i < args.Length; i++)
{
var argument = args[i];
if (TrySplitOption(argument, "--console", out var consoleText))
{
if (!string.Equals(consoleText, "ps5", StringComparison.OrdinalIgnoreCase))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
continue;
}
if (TrySplitOption(argument, "--console-language", out var consoleLanguageText))
{
if (!TryParseSystemLanguage(consoleLanguageText, out systemLanguage))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
continue;
}
if (TrySplitOption(argument, "--window-mode", out var windowModeText))
{
if (!TryParseWindowMode(windowModeText, out var windowMode))
@ -1178,41 +1133,6 @@ internal static partial class Program
vsyncOverride = vsync;
continue;
}
if (TrySplitOption(argument, "--overlay", out var overlayText))
{
if (!TryParseSwitch(overlayText, out var overlayEnabled))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
overlayEnabledOverride = overlayEnabled;
continue;
}
if (TrySplitOption(argument, "--overlay-mode", out var overlayModeText))
{
if (!Enum.TryParse<PerformanceOverlayMode>(overlayModeText, true, out var overlayMode) ||
!Enum.IsDefined(overlayMode))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
overlayModeOverride = overlayMode;
continue;
}
if (TrySplitOption(argument, "--overlay-corner", out var overlayCornerText))
{
if (!Enum.TryParse<PerformanceOverlayCorner>(overlayCornerText, true, out var overlayCorner) ||
!Enum.IsDefined(overlayCorner))
{
ebootPath = string.Empty;
runtimeOptions = default;
return false;
}
overlayCornerOverride = overlayCorner;
continue;
}
if (TrySplitOption(argument, "--hdr", out var hdrText))
{
if (!TryParseHdrMode(hdrText, out var hdrMode))
@ -1382,7 +1302,6 @@ internal static partial class Program
runtimeOptions = new SharpEmuRuntimeOptions
{
CpuEngine = cpuEngine,
SystemLanguage = systemLanguage,
StrictDynlibResolution = strictDynlibResolution,
ImportTraceLimit = importTraceLimit,
};
@ -1397,9 +1316,6 @@ internal static partial class Program
RefreshRate = refreshRateOverride ?? configuredVideoOptions.RefreshRate,
VSync = vsyncOverride ?? configuredVideoOptions.VSync,
HdrMode = hdrModeOverride ?? configuredVideoOptions.HdrMode,
OverlayEnabled = overlayEnabledOverride ?? configuredVideoOptions.OverlayEnabled,
OverlayMode = overlayModeOverride ?? configuredVideoOptions.OverlayMode,
OverlayCorner = overlayCornerOverride ?? configuredVideoOptions.OverlayCorner,
}).Normalize();
return true;
}
@ -1437,11 +1353,6 @@ internal static partial class Program
RefreshRate = effective.RefreshRate,
VSync = effective.VSync,
HdrMode = hdrMode,
OverlayEnabled = effective.OverlayEnabled,
OverlayCorner = Enum.TryParse<PerformanceOverlayCorner>(effective.OverlayCorner, true, out var corner)
? corner : defaults.OverlayCorner,
OverlayMode = Enum.TryParse<PerformanceOverlayMode>(effective.OverlayMode, true, out var mode)
? mode : defaults.OverlayMode,
}.Normalize();
}
catch (Exception exception)
@ -1477,46 +1388,6 @@ internal static partial class Program
return Enum.IsDefined(mode);
}
private static bool TryParseSystemLanguage(string value, out int language)
{
language = value.ToLowerInvariant() switch
{
"japanese" => 0,
"englishus" => 1,
"french" => 2,
"spanishspain" => 3,
"german" => 4,
"italian" => 5,
"dutch" => 6,
"portugueseportugal" => 7,
"russian" => 8,
"korean" => 9,
"chinesetraditional" => 10,
"chinesesimplified" => 11,
"finnish" => 12,
"swedish" => 13,
"danish" => 14,
"norwegian" => 15,
"polish" => 16,
"portuguesebrazil" => 17,
"englishuk" => 18,
"turkish" => 19,
"spanishlatinamerica" => 20,
"arabic" => 21,
"frenchcanada" => 22,
"czech" => 23,
"hungarian" => 24,
"greek" => 25,
"romanian" => 26,
"thai" => 27,
"vietnamese" => 28,
"indonesian" => 29,
"ukrainian" => 30,
_ => -1,
};
return language >= 0;
}
private static bool TryParseScalingMode(string value, out HostScalingMode mode)
{
mode = value.ToLowerInvariant() switch

View File

@ -133,20 +133,6 @@ public sealed class CpuDispatcher : ICpuDispatcher, IDisposable
}
}
public bool TryInstallAdditionalModule(
IReadOnlyDictionary<ulong, string> importStubs,
IReadOnlyDictionary<string, ulong> runtimeSymbols,
out string? error)
{
if (_nativeCpuBackend is not DirectExecutionBackend backend)
{
error = "the active CPU backend does not support runtime modules";
return false;
}
return backend.TryInstallAdditionalModule(importStubs, runtimeSymbols, out error);
}
private OrbisGen2Result DispatchEntryCore(
ulong entryPoint,
Generation generation,

View File

@ -1,149 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Numerics;
namespace SharpEmu.Core.Cpu.Emulation;
/// <summary>
/// Calculates the results of the SHA extension instructions for instruction recovery.
/// Dword 0 of a value is bits 31:0 and dword 3 is bits 127:96, as in the Intel SDM.
/// </summary>
public static class ShaInstructionEmulator
{
public static UInt128 Sha1Rnds4(UInt128 source1, UInt128 source2, int function)
{
var a = Dword(source1, 3);
var b = Dword(source1, 2);
var c = Dword(source1, 1);
var d = Dword(source1, 0);
var e = 0u;
var k = (function & 3) switch
{
0 => 0x5A827999u,
1 => 0x6ED9EBA1u,
2 => 0x8F1BBCDCu,
_ => 0xCA62C1D6u,
};
// The first message dword already carries E (SHA1NEXTE adds it).
for (var round = 0; round < 4; round++)
{
var next = Sha1Function(function, b, c, d) + BitOperations.RotateLeft(a, 5) + Dword(source2, 3 - round) + e + k;
e = d;
d = c;
c = BitOperations.RotateLeft(b, 30);
b = a;
a = next;
}
return FromDwords(d, c, b, a);
}
public static UInt128 Sha1Nexte(UInt128 source1, UInt128 source2)
{
var e = BitOperations.RotateLeft(Dword(source1, 3), 30);
return FromDwords(Dword(source2, 0), Dword(source2, 1), Dword(source2, 2), Dword(source2, 3) + e);
}
public static UInt128 Sha1Msg1(UInt128 source1, UInt128 source2)
{
var w0 = Dword(source1, 3);
var w1 = Dword(source1, 2);
var w2 = Dword(source1, 1);
var w3 = Dword(source1, 0);
var w4 = Dword(source2, 3);
var w5 = Dword(source2, 2);
return FromDwords(w5 ^ w3, w4 ^ w2, w3 ^ w1, w2 ^ w0);
}
public static UInt128 Sha1Msg2(UInt128 source1, UInt128 source2)
{
var w13 = Dword(source2, 2);
var w14 = Dword(source2, 1);
var w15 = Dword(source2, 0);
var w16 = BitOperations.RotateLeft(Dword(source1, 3) ^ w13, 1);
var w17 = BitOperations.RotateLeft(Dword(source1, 2) ^ w14, 1);
var w18 = BitOperations.RotateLeft(Dword(source1, 1) ^ w15, 1);
var w19 = BitOperations.RotateLeft(Dword(source1, 0) ^ w16, 1);
return FromDwords(w19, w18, w17, w16);
}
// SHA256RNDS2 takes its two round-constant-plus-message dwords from XMM0[63:0].
public static UInt128 Sha256Rnds2(UInt128 source1, UInt128 source2, UInt128 xmm0)
{
var a = Dword(source2, 3);
var b = Dword(source2, 2);
var c = Dword(source1, 3);
var d = Dword(source1, 2);
var e = Dword(source2, 1);
var f = Dword(source2, 0);
var g = Dword(source1, 1);
var h = Dword(source1, 0);
for (var round = 0; round < 2; round++)
{
var t1 = Ch(e, f, g) + BigSigma1(e) + Dword(xmm0, round) + h;
var t2 = Maj(a, b, c) + BigSigma0(a);
h = g;
g = f;
f = e;
e = d + t1;
d = c;
c = b;
b = a;
a = t1 + t2;
}
return FromDwords(f, e, b, a);
}
public static UInt128 Sha256Msg1(UInt128 source1, UInt128 source2)
{
var w4 = Dword(source2, 0);
var w3 = Dword(source1, 3);
var w2 = Dword(source1, 2);
var w1 = Dword(source1, 1);
var w0 = Dword(source1, 0);
return FromDwords(w0 + SmallSigma0(w1), w1 + SmallSigma0(w2), w2 + SmallSigma0(w3), w3 + SmallSigma0(w4));
}
public static UInt128 Sha256Msg2(UInt128 source1, UInt128 source2)
{
var w14 = Dword(source2, 2);
var w15 = Dword(source2, 3);
var w16 = Dword(source1, 0) + SmallSigma1(w14);
var w17 = Dword(source1, 1) + SmallSigma1(w15);
var w18 = Dword(source1, 2) + SmallSigma1(w16);
var w19 = Dword(source1, 3) + SmallSigma1(w17);
return FromDwords(w16, w17, w18, w19);
}
public static uint Dword(UInt128 value, int index) => (uint)(value >> (32 * index));
public static UInt128 FromDwords(uint dword0, uint dword1, uint dword2, uint dword3) =>
((UInt128)dword3 << 96) | ((UInt128)dword2 << 64) | ((UInt128)dword1 << 32) | dword0;
private static uint Sha1Function(int function, uint b, uint c, uint d) => (function & 3) switch
{
0 => (b & c) ^ (~b & d),
2 => (b & c) ^ (b & d) ^ (c & d),
_ => b ^ c ^ d,
};
private static uint Ch(uint e, uint f, uint g) => (e & f) ^ (~e & g);
private static uint Maj(uint a, uint b, uint c) => (a & b) ^ (a & c) ^ (b & c);
private static uint BigSigma0(uint x) =>
BitOperations.RotateRight(x, 2) ^ BitOperations.RotateRight(x, 13) ^ BitOperations.RotateRight(x, 22);
private static uint BigSigma1(uint x) =>
BitOperations.RotateRight(x, 6) ^ BitOperations.RotateRight(x, 11) ^ BitOperations.RotateRight(x, 25);
private static uint SmallSigma0(uint x) =>
BitOperations.RotateRight(x, 7) ^ BitOperations.RotateRight(x, 18) ^ (x >> 3);
private static uint SmallSigma1(uint x) =>
BitOperations.RotateRight(x, 17) ^ BitOperations.RotateRight(x, 19) ^ (x >> 10);
}

View File

@ -4,7 +4,23 @@
namespace SharpEmu.Core.Cpu.Emulation;
/// <summary>
/// Calculates the bit-field results for SSE4a instruction recovery.
/// Pure software implementation of the bit-field math behind AMD's SSE4a EXTRQ/INSERTQ
/// (immediate-form) instructions.
///
/// The direct-execution backend runs guest PS5 code natively on the host CPU. The PS5's Zen 2
/// cores implement AMD-only SSE4a (EXTRQ/INSERTQ), but Intel hosts - and Rosetta 2 on Apple
/// Silicon - do not, so they raise #UD (STATUS_ILLEGAL_INSTRUCTION) instead of executing the
/// opcode. SharpEmu already rewrites one specific compiled EXTRQ+VPBLENDD idiom at load time
/// (see <see cref="Native.Sse4aExtrqBlendPatch"/>), but any other occurrence of EXTRQ/INSERTQ -
/// a different register allocation, a title built with a different compiler version, and so on
/// - still aborts the title. This class ported from Kyty's
/// <c>Loader::X64InstructionEmulator::TryEmulateSse4a</c> provides the general bit-field
/// extract/insert so the illegal-instruction handler can finish *any* immediate-form
/// EXTRQ/INSERTQ in software and resume, instead of relying on a single hard-coded byte pattern.
///
/// The methods operate on plain 64-bit integers rather than the OS CONTEXT record so the bit
/// math can be unit-tested in isolation; the unsafe CONTEXT/XMM plumbing lives in the backend
/// adapter (<see cref="Native.DirectExecutionBackend"/>).
/// </summary>
public static class Sse4aBitFieldEmulator
{
@ -19,13 +35,16 @@ public static class Sse4aBitFieldEmulator
{
var len = length & 0x3F;
var idx = index & 0x3F;
if (len == 0)
if (!IsValidBitField(length, index))
{
len = 64;
return 0;
}
if (len == 0)
{
return value;
}
// Results past bit 63 are undefined; limit the field to the remaining bits.
len = Math.Min(len, 64 - idx);
var mask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
return (value >> idx) & mask;
}
@ -34,12 +53,16 @@ public static class Sse4aBitFieldEmulator
{
var len = length & 0x3F;
var idx = index & 0x3F;
if (len == 0)
if (!IsValidBitField(length, index))
{
len = 64;
return destination;
}
if (len == 0)
{
return source;
}
len = Math.Min(len, 64 - idx);
var fieldMask = len == 64 ? ulong.MaxValue : (1UL << len) - 1;
var destinationClearMask = fieldMask << idx;
var sourceField = (source & fieldMask) << idx;

View File

@ -26,21 +26,29 @@ public sealed unsafe class CpuPatcher : IDisposable
internal unsafe class UnsafeCodeReader : Iced.Intel.CodeReader
{
private readonly byte* _start;
private readonly ulong _length;
public ulong Position { get; private set; }
private byte* _start;
private byte* _current;
private int _length;
public UnsafeCodeReader(byte* start, ulong length)
public UnsafeCodeReader(byte* start, int length)
{
_start = start;
_current = start;
_length = length;
}
public override int ReadByte()
{
if (Position >= _length)
if (_current >= _start + _length)
return -1;
return _start[Position++];
return *_current++;
}
public int Position => (int)(_current - _start);
public void Skip(int count)
{
_current += Math.Min(Math.Max(count, 0), _length - Position);
}
}
}

View File

@ -7,8 +7,23 @@ using SharpEmu.Core.Cpu.Emulation;
namespace SharpEmu.Core.Cpu.Native;
// Recover supported instructions only after an illegal-instruction fault.
// Leave native instructions and unrelated register values unchanged.
// General software fallback for the AMD-only instructions PS5 titles occasionally emit that a
// Zen 2-only host implements but Intel hosts (and Rosetta 2 on Apple Silicon) do not:
// - SSE4a EXTRQ/INSERTQ, immediate form
// - MONITORX/MWAITX
//
// This is a direct port of Kyty's Loader::X64InstructionEmulator (TryEmulateSse4a /
// TryEmulateMonitorxMwaitx). SharpEmu already special-cases exactly one compiled EXTRQ+VPBLENDD
// byte sequence at load time (Sse4aExtrqBlendPatch), which only helps the one idiom it was
// reverse-engineered from. This file is a general, fault-time fallback that engages for any
// immediate-form EXTRQ/INSERTQ or MONITORX/MWAITX the narrower patch (or a title using a
// different compiler/register allocation) does not cover, complementing rather than replacing
// it: the load-time patch still avoids paying the fault-and-recover cost on the hot path it was
// built for, while this method is the safety net for everything else.
//
// This is deliberately additive: DirectExecutionBackend.IllegalInstruction.cs (the BMI1/BMI2/ABM
// fallback) is untouched, and this method is only reached from VectoredHandler after that one
// has already declined to handle the fault.
public sealed partial class DirectExecutionBackend
{
// Byte offset of Xmm0 within the Win64 CONTEXT record: FltSave (the XMM_SAVE_AREA32/FXSAVE
@ -31,8 +46,16 @@ public sealed partial class DirectExecutionBackend
return true;
}
// EXTRQ and INSERTQ need the current vector register values.
// Use recovery only when the signal context contains these values.
// MONITORX/MWAITX above only ever reads guest code memory and rewrites RIP, both of
// which the POSIX signal bridge (DirectExecutionBackend.PosixSignals.cs) faithfully
// round-trips through the real ucontext, so it works on every supported OS. EXTRQ/
// INSERTQ additionally read and write an XMM register: on Windows contextRecord is the
// live CONTEXT the OS resumes the thread from, so touching the Xmm0.. slots is visible
// to the guest, and on Linux the bridge copies the mcontext's FXSAVE image into the
// Xmm0.. slots and writes them back through sigreturn (_posixXmmContextBridged). On
// Darwin the XMM area is still a zeroed scratch buffer - running this there would
// silently compute a result from stale bytes and then discard whatever it "wrote", so
// the recovery declines until that bridge exists.
return (OperatingSystem.IsWindows() || _posixXmmContextBridged) &&
TryRecoverSse4aExtractInsert(contextRecord, rip);
}
@ -42,7 +65,7 @@ public sealed partial class DirectExecutionBackend
// MONITORX (0F 01 FA) and MWAITX (0F 01 FB) are fixed 3-byte encodings with no
// ModRM/SIB/displacement/immediate, so a raw byte compare is sufficient and unambiguous.
var opcode = new byte[3];
if (!TryReadExecutableBytes(rip, opcode) ||
if (!TryReadHostBytes(rip, opcode) ||
opcode[0] != 0x0F || opcode[1] != 0x01 || (opcode[2] != 0xFA && opcode[2] != 0xFB))
{
return false;
@ -88,10 +111,7 @@ public sealed partial class DirectExecutionBackend
return false;
}
var isImmediateExtrq = isExtrq && instruction.OpCount == 3;
var isRegisterExtrq = isExtrq && instruction.OpCount == 2;
var isImmediateInsertq = isInsertq && instruction.OpCount == 4;
if (!isImmediateExtrq && !isRegisterExtrq && !isImmediateInsertq)
if (isExtrq && instruction.OpCount != 3 || isInsertq && instruction.OpCount != 4)
{
return false;
}
@ -105,26 +125,11 @@ public sealed partial class DirectExecutionBackend
var destLow = ReadCtxU64(contextRecord, destOffset);
if (isExtrq)
{
int length;
int index;
if (isRegisterExtrq)
var length = (int)instruction.GetImmediate(1);
var index = (int)instruction.GetImmediate(2);
if (!Sse4aBitFieldEmulator.IsValidBitField(length, index))
{
if (instruction.GetOpKind(1) != OpKind.Register ||
!TryGetXmmOffset(instruction.GetOpRegister(1), out var controlOffset))
{
return false;
}
// AMD defines the register form's field length in xmm2[5:0] and its start
// index in xmm2[13:8]. Other control bits do not affect the instruction.
var control = ReadCtxU64(contextRecord, controlOffset);
length = (int)(control & 0x3F);
index = (int)((control >> 8) & 0x3F);
}
else
{
length = (int)instruction.GetImmediate(1);
index = (int)instruction.GetImmediate(2);
return false;
}
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.ExtractBitField(destLow, length, index));
@ -140,6 +145,11 @@ public sealed partial class DirectExecutionBackend
var length = (int)instruction.GetImmediate(2);
var index = (int)instruction.GetImmediate(3);
if (!Sse4aBitFieldEmulator.IsValidBitField(length, index))
{
return false;
}
WriteCtxU64(contextRecord, destOffset, Sse4aBitFieldEmulator.InsertBitField(
destLow, ReadCtxU64(contextRecord, srcOffset), length, index));
WriteCtxU64(contextRecord, destOffset + 8, 0);

View File

@ -88,14 +88,6 @@ public sealed partial class DirectExecutionBackend
return $"{kvp.Key}: {cores:F2}cores {seconds:F1}s n={callCount} {perCallUs:F2}us/call";
});
System.Console.Error.WriteLine($"[PERF][HLE] cost: {string.Join(" | ", top)}");
if (SharpEmu.HLE.GuestFastPath.Enabled)
{
var fastPath = SharpEmu.HLE.GuestFastPath.SnapshotCounters();
System.Console.Error.WriteLine(
$"[PERF][HLE] fast_path: self_hits={fastPath.SelfHits} " +
$"getspecific_hits={fastPath.GetspecificHits} blocks={fastPath.Blocks}");
}
}
}
@ -471,29 +463,6 @@ public sealed partial class DirectExecutionBackend
return false;
}
// The committed readable run of host pages that contains address.
private unsafe static bool TryQueryReadableRange(ulong address, out ulong start, out ulong end)
{
start = 0;
end = 0;
if (address <= 65536 || address >= 140737488355328L ||
VirtualQuery((void*)address, out var info, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
return false;
}
var regionEnd = info.BaseAddress + info.RegionSize;
if (regionEnd < info.BaseAddress || address > regionEnd - 8 ||
info.State != 4096 || !IsReadableProtection(info.Protect))
{
return false;
}
start = info.BaseAddress;
end = regionEnd;
return true;
}
private unsafe static bool TryReadStackU64(ulong address, out ulong value)
{
value = 0uL;

View File

@ -10,7 +10,6 @@ using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.HLE;
using SharpEmu.HLE.GpuMemory;
namespace SharpEmu.Core.Cpu.Native;
@ -41,6 +40,15 @@ public sealed partial class DirectExecutionBackend
}
_rawExceptionHandler = (nint)AddVectoredExceptionHandler(1u, _rawExceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Raw exception handler installed: 0x{_rawExceptionHandler:X16}");
// The raw handler carries the guest-image write-fault bridge, so the
// path must be compiled before the first protected-page store can
// reach it. Guest code has not started yet, so warming here cannot
// race a real fault.
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
Console.Error.WriteLine(
"[LOADER][INFO] Guest image CPU write tracking: " +
$"{(SharpEmu.HLE.GuestImageWriteTracker.Enabled ? "enabled" : "disabled")}");
}
else
{
@ -56,6 +64,7 @@ public sealed partial class DirectExecutionBackend
}
_exceptionHandler = (nint)AddVectoredExceptionHandler(1u, _exceptionHandlerStub);
Console.Error.WriteLine($"[LOADER][INFO] Exception handler installed: 0x{_exceptionHandler:X16}");
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
_unhandledFilterDelegate = UnhandledExceptionFilter;
_unhandledFilterHandle = GCHandle.Alloc(_unhandledFilterDelegate);
@ -114,19 +123,22 @@ public sealed partial class DirectExecutionBackend
ulong rip = ReadCtxU64(contextRecord, 248);
ulong rsp = ReadCtxU64(contextRecord, 152);
if (MayBeGuestInt41(exceptionRecord) && TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
if (TryRecoverGuestInt41(exceptionCode, contextRecord, rip))
{
return -1;
}
if (!OperatingSystem.IsWindows() &&
exceptionCode == 3221225477u &&
if (exceptionCode == 3221225477u &&
exceptionRecord->NumberParameters >= 2 &&
exceptionRecord->ExceptionInformation[0] == 1uL &&
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
exceptionRecord->ExceptionInformation[1]))
{
return -1;
}
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
{
return -1;
}
if (exceptionCode == 3221225477u && TryHandleLazyCommittedPage(exceptionRecord, rip, rsp))
{
return -1;
@ -136,14 +148,8 @@ public sealed partial class DirectExecutionBackend
{
return -1;
}
if (exceptionCode == 3221225477u && TryResolveGpuFault(exceptionRecord))
{
return -1;
}
// Only a fault none of the handlers above resolves aborts a TBB worker: a write
// to a lazily committed or GPU-tracked page is routine, and aborting the worker
// there strands the guest allocator lock it may hold.
if (TryRecoverAuxiliaryThreadExecuteFault(exceptionRecord, contextRecord, rip))
if (exceptionCode == 3221225477u &&
TryRecoverInflateFastEpilogue(exceptionRecord, contextRecord, rip, rsp))
{
return -1;
}
@ -157,11 +163,6 @@ public sealed partial class DirectExecutionBackend
{
return -1;
}
if (exceptionCode == StatusIllegalInstruction &&
TryRecoverShaInstruction(contextRecord, rip))
{
return -1;
}
if (IsBenignHostDebugException(exceptionCode))
{
return -1;
@ -284,7 +285,7 @@ public sealed partial class DirectExecutionBackend
for (int i = 0; i < 16; i++)
{
ulong stackAddr = rsp + (ulong)(i * 8);
if (!TryReadDiagnosticHostQword(stackAddr, out ulong value))
if (!TryReadHostQword(stackAddr, out ulong value))
{
Console.Error.WriteLine("[LOADER][WARNING] Could not read stack qwords.");
break;
@ -301,7 +302,7 @@ public sealed partial class DirectExecutionBackend
var windowStart = rsp >= 0x300 ? rsp - 0x300 : 0;
for (var stackAddr = windowStart; stackAddr < rsp + 0x100; stackAddr += 8)
{
if (!TryReadDiagnosticHostQword(stackAddr, out var value))
if (!TryReadHostQword(stackAddr, out var value))
{
continue;
}
@ -326,18 +327,18 @@ public sealed partial class DirectExecutionBackend
DumpPointerWindow("fault-register-r13", r13, 0x60);
DumpPointerWindow("fault-register-r14", r14, 0x60);
try
{
Console.Error.WriteLine("[LOADER][INFO] Frame chain (RBP walk):");
ulong frame = rbp;
for (int i = 0; i < 12; i++)
{
if (!IsCanonicalUserAddress(frame) || (frame & 7) != 0)
if (frame < 0x10000)
{
break;
}
if (!TryReadDiagnosticHostQword(frame, out ulong next) ||
!TryReadDiagnosticHostQword(frame + 8, out ulong ret))
if (!TryReadHostQword(frame, out ulong next) || !TryReadHostQword(frame + 8, out ulong ret))
{
Console.Error.WriteLine("[LOADER][WARNING] Could not walk RBP frame chain.");
break;
@ -365,13 +366,7 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine("[LOADER][ERROR] - Guest code accessed unmapped memory");
Console.Error.WriteLine("[LOADER][ERROR] - Need to implement HLE for this NID");
byte[] code = new byte[16];
DumpHostRegion("RIP", rip);
// Guest code can be mapped execute-only, which makes every
// host-side read of it fail. The guest memory view still
// resolves those pages, so prefer it here: otherwise the
// whole call-site report below is silently skipped on
// exactly the faults that need it.
if (TryReadGuestOrHostBytes(rip, code))
if (TryReadHostBytes(rip, code))
{
Console.Error.WriteLine("[LOADER][INFO] Code at RIP: " + BitConverter.ToString(code).Replace("-", " "));
if (code[0] == 100)
@ -389,12 +384,12 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine($"[LOADER][INFO] RSP: 0x{rsp:X16} (mod 16 = {rsp % 16})");
}
byte[] before = new byte[16];
if (rip > 16 && TryReadGuestOrHostBytes(rip - 16, before))
if (rip > 16 && TryReadHostBytes(rip - 16, before))
{
Console.Error.WriteLine("[LOADER][INFO] Code before RIP: " + BitConverter.ToString(before).Replace("-", " "));
}
byte[] window = new byte[64];
if (rip > 32 && TryReadGuestOrHostBytes(rip - 32, window))
if (rip > 32 && TryReadHostBytes(rip - 32, window))
{
Console.Error.WriteLine("[LOADER][INFO] Code window [RIP-0x20..]: " + BitConverter.ToString(window).Replace("-", " "));
}
@ -411,7 +406,7 @@ public sealed partial class DirectExecutionBackend
continue;
}
byte[] callSiteWindow = new byte[48];
if (TryReadGuestOrHostBytes(candidate - 24, callSiteWindow))
if (TryReadHostBytes(candidate - 24, callSiteWindow))
{
Console.Error.WriteLine(
$"[LOADER][INFO] Stack guest-code candidate [rsp+0x{stackIndex * 8:X2}]=0x{candidate:X16}, bytes [-0x18..]: " +
@ -423,11 +418,6 @@ public sealed partial class DirectExecutionBackend
{
Console.Error.WriteLine("[LOADER][ERROR] Could not read code at RIP");
}
// The faulting instruction is the first thing an access
// violation report has to answer, so decode it through guest
// memory as well: that path still works when the host code
// page refuses a plain read.
DumpGuestInstructionStream("fault", rip, 4);
DumpRecentImportTrace();
DumpGuestDisasmDiagnostics(rip, rbp, rsp);
DumpGuestRegisterWindowDiagnostics(
@ -619,16 +609,6 @@ public sealed partial class DirectExecutionBackend
return (ulong)(_workerAbortStack + (nint)WorkerAbortStackSize - 0x100) & ~0xFUL;
}
// On Windows an INT n the guest may not execute raises a general-protection fault,
// reported as an access violation whose fault address is all ones; a page fault
// carries the data address instead. GPU-tracked and lazily committed pages fault
// thousands of times per frame, so skip the opcode probe (a VirtualQuery and a
// read of the faulting instruction) for them.
private static unsafe bool MayBeGuestInt41(EXCEPTION_RECORD* exceptionRecord) =>
!OperatingSystem.IsWindows() ||
exceptionRecord->NumberParameters < 2 ||
exceptionRecord->ExceptionInformation[1] == ulong.MaxValue;
private unsafe bool TryRecoverGuestInt41(uint exceptionCode, void* contextRecord, ulong rip)
{
if (!_ignoreGuestInt41 || exceptionCode != 3221225477u || rip < 0x10000)
@ -637,7 +617,7 @@ public sealed partial class DirectExecutionBackend
}
byte[] opcode = new byte[2];
if (!TryReadExecutableBytes(rip, opcode) || opcode[0] != 0xCD || opcode[1] != 0x41)
if (!TryReadHostBytes(rip, opcode) || opcode[0] != 0xCD || opcode[1] != 0x41)
{
return false;
}
@ -653,25 +633,6 @@ public sealed partial class DirectExecutionBackend
return true;
}
// Runs after the lazy-commit branch. That branch commits the page first.
private unsafe static bool TryResolveGpuFault(EXCEPTION_RECORD* exceptionRecord)
{
if (exceptionRecord->NumberParameters < 2)
{
return false;
}
var kind = exceptionRecord->ExceptionInformation[0] switch
{
0 => FaultKind.Read,
1 => FaultKind.Write,
8 => FaultKind.Execute,
_ => FaultKind.Unknown,
};
return kind != FaultKind.Unknown
&& GuestGpuMemoryHook.TryResolveFault(kind, exceptionRecord->ExceptionInformation[1]);
}
private unsafe static bool TryRecoverGuestAllocatorHole(
EXCEPTION_RECORD* exceptionRecord,
void* contextRecord,
@ -715,6 +676,179 @@ public sealed partial class DirectExecutionBackend
return true;
}
private unsafe bool TryRecoverInflateFastEpilogue(EXCEPTION_RECORD* exceptionRecord, void* contextRecord, ulong rip, ulong rsp)
{
// Only apply this compat recovery for Ghostrunner (PPSA03685)
var currentTitleId = CurrentTitleId ?? SharpEmu.Libs.VideoOut.VideoOutExports.GetApplicationTitleId();
if (!string.Equals(currentTitleId, "PPSA03685", StringComparison.OrdinalIgnoreCase))
{
return false;
}
const ulong epilogueStoreRip = 0x0000000804A2C23EuL;
const ulong epilogueStateStoreRip = 0x0000000804A2C25AuL;
if (rip != epilogueStoreRip && rip != epilogueStateStoreRip)
{
return false;
}
// Check if this is the state fault at 0x804A2C25A (mov [rcx + 0x48], rax)
if (rip == epilogueStateStoreRip)
{
ulong rcxFault = ReadCtxU64(contextRecord, CTX_RCX);
if (rcxFault >= 0x10000UL && exceptionRecord->ExceptionInformation[1] != 0x48)
{
return false;
}
Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE STATE RECOVERY TRIGGERED AT 0x804A2C25A <<<");
// Recover strm from rdi or caller frame
ulong strm = ReadCtxU64(contextRecord, CTX_RDI);
if (strm < 0x10000UL)
{
if (TryReadQword(rsp + 0x108, out ulong candidate) && candidate >= 0x10000)
{
strm = candidate;
}
else if (TryReadQword(rsp + 0x80, out ulong savedRbp))
{
if (TryReadQword((ulong)((long)savedRbp - 0x58), out ulong candidate2) && candidate2 >= 0x10000)
{
strm = candidate2;
}
else if (TryReadQword(savedRbp + 0x20, out ulong candidate3) && candidate3 >= 0x10000)
{
strm = candidate3;
}
}
}
if (strm < 0x10000UL)
{
Console.Error.WriteLine("[LOADER][ERROR] Could not locate strm for state recovery at 0x804A2C25A!");
return false;
}
ulong state = *(ulong*)(strm + 0x38);
if (state < 0x10000UL)
{
state = *(ulong*)(strm + 0x28);
}
if (state < 0x10000UL)
{
Console.Error.WriteLine($"[LOADER][ERROR] Invalid state pointer from strm (0x{strm:X16}): 0x{state:X16}");
return false;
}
ulong raxVal = ReadCtxU64(contextRecord, CTX_RAX);
ulong r8Val = ReadCtxU64(contextRecord, CTX_R8);
*(ulong*)(state + 0x48) = raxVal;
*(uint*)(state + 0x50) = (uint)r8Val;
WriteCtxU64(contextRecord, CTX_RCX, state);
WriteCtxU64(contextRecord, CTX_RIP, 0x0000000804A2C262uL);
Console.Error.WriteLine($"[LOADER][INFO] Recovered state: 0x{state:X16}");
Console.Error.WriteLine($"[LOADER][INFO] Executed state->hold = 0x{raxVal:X16}, state->bits = {r8Val & 0xFF}");
Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE STATE RECOVERY COMPLETED - RESUMING <<<");
Console.Error.Flush();
return true;
}
// Otherwise, fault at 0x804A2C23E
ulong rdiVal = ReadCtxU64(contextRecord, CTX_RDI);
if (rdiVal != 0)
{
return false;
}
Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE RECOVERY TRIGGERED AT 0x804A2C23E <<<");
// 1. Recover strm from caller frame:
// In inflate_fast frame:
// [rsp + 0x80] is caller's saved RBP
// [rsp + 0x108] holds strm (0x00007FFFF01FF470)
ulong strmVal = 0;
if (TryReadQword(rsp + 0x108, out ulong cand) && cand >= 0x10000)
{
strmVal = cand;
}
if (strmVal == 0 && TryReadQword(rsp + 0x80, out ulong savedRbpVal))
{
if (TryReadQword((ulong)((long)savedRbpVal - 0x58), out ulong cand2) && cand2 >= 0x10000)
{
strmVal = cand2;
}
else if (TryReadQword(savedRbpVal + 0x20, out ulong cand3) && cand3 >= 0x10000)
{
strmVal = cand3;
}
}
if (strmVal == 0)
{
Console.Error.WriteLine("[LOADER][ERROR] Could not locate strm in stack frame for inflate_fast recovery!");
return false;
}
Console.Error.WriteLine($"[LOADER][INFO] Recovered strm: 0x{strmVal:X16}");
// 2. Set rdi = strm in CPU context
WriteCtxU64(contextRecord, CTX_RDI, strmVal);
// 3. If r10 is invalid or underflowed, recover actual input position
ulong r10Val = ReadCtxU64(contextRecord, CTX_R10);
bool isInvalidInPointer = (r10Val < 0x10000UL) || (r10Val > 0x00007FFFFFFFFFFEUL);
if (isInvalidInPointer)
{
ulong inEnd = 0;
if (TryReadQword(rsp, out inEnd) && inEnd >= 0x10000)
{
ulong raxVal = ReadCtxU64(contextRecord, CTX_RAX);
ulong unusedBytes = raxVal & 0xFFFFFFFFUL; // bits >> 3
ulong recoveredNextIn = inEnd - unusedBytes + 1;
r10Val = recoveredNextIn - 1;
WriteCtxU64(contextRecord, CTX_R10, r10Val);
WriteCtxU64(contextRecord, CTX_RDX, recoveredNextIn);
Console.Error.WriteLine($"[LOADER][INFO] Recovered r10 (in): 0x{r10Val:X16} (in_end=0x{inEnd:X16}, unused={unusedBytes})");
}
}
// 4. Recover rcx (strm->state) if needed so subsequent stores at 0x804A2C25A succeed
ulong rcxVal = ReadCtxU64(contextRecord, CTX_RCX);
if (rcxVal < 0x10000UL)
{
ulong state = *(ulong*)(strmVal + 0x38);
if (state < 0x10000UL)
{
state = *(ulong*)(strmVal + 0x28);
}
if (state >= 0x10000UL)
{
WriteCtxU64(contextRecord, CTX_RCX, state);
Console.Error.WriteLine($"[LOADER][INFO] Recovered rcx (strm->state): 0x{state:X16}");
}
}
// 5. Perform the stores directly to ensure stream consistency
ulong rdxVal = ReadCtxU64(contextRecord, CTX_RDX);
ulong r9Val = ReadCtxU64(contextRecord, CTX_R9);
*(ulong*)strmVal = rdxVal;
*(ulong*)(strmVal + 0x18) = r9Val + 1;
Console.Error.WriteLine($"[LOADER][INFO] Executed strm->next_in = 0x{rdxVal:X16}");
Console.Error.WriteLine($"[LOADER][INFO] Executed strm->next_out = 0x{r9Val + 1:X16}");
Console.Error.WriteLine("[LOADER][INFO] >>> INFLATE_FAST EPILOGUE RECOVERY COMPLETED - RESUMING <<<");
Console.Error.Flush();
return true;
}
private static bool IsBenignHostDebugException(uint exceptionCode)
{
return exceptionCode is DBG_PRINTEXCEPTION_C or DBG_PRINTEXCEPTION_WIDE_C or MS_VC_THREADNAME_EXCEPTION;
@ -809,7 +943,7 @@ public sealed partial class DirectExecutionBackend
// Optimized guest code frequently omits frame pointers. The return
// address at RSP is then more useful than an RBP walk and identifies the
// exact call site that supplied the faulting arguments.
if (TryReadDiagnosticHostQword(rsp, out var stackReturn) && stackReturn >= 0x60)
if (TryReadHostQword(rsp, out var stackReturn) && stackReturn >= 0x60)
{
DumpGuestInstructionStream("stack-return-prelude", stackReturn - 0x60, 40);
}
@ -1238,7 +1372,7 @@ public sealed partial class DirectExecutionBackend
for (int offset = 0; offset < size; offset += 8)
{
ulong slotAddress = baseAddress + (ulong)offset;
if (slotAddress < baseAddress || !TryReadDiagnosticHostQword(slotAddress, out var value))
if (!TryReadQword(slotAddress, out var value))
{
Console.Error.WriteLine($"[LOADER][INFO] +0x{offset:X2}: <unreadable>");
break;
@ -1279,18 +1413,12 @@ public sealed partial class DirectExecutionBackend
}
}
private static bool TryReadHostQword(ulong address, out ulong value)
private unsafe static bool TryReadHostQword(ulong address, out ulong value)
{
value = 0;
if (address < 0x10000)
{
return false;
}
if (address < 65536) return false;
if (!OperatingSystem.IsWindows())
{
// A stray read inside the signal handler would raise a nested
// SIGSEGV and kill the process before diagnostics finish, so
// probe the region table instead of relying on try/catch.
return TryReadStackU64(address, out value);
}
@ -1305,90 +1433,24 @@ public sealed partial class DirectExecutionBackend
}
}
private unsafe static bool TryReadDiagnosticHostQword(ulong address, out ulong value)
{
if (OperatingSystem.IsLinux())
{
ulong result = 0;
bool success = TryReadLinuxMemory(address, (byte*)&result, sizeof(ulong));
value = success ? result : 0;
return success;
}
if (OperatingSystem.IsMacOS())
{
ulong result = 0;
bool success = TryReadMacOsMemory(address, (byte*)&result, sizeof(ulong));
value = success ? result : 0;
return success;
}
if (!OperatingSystem.IsWindows())
{
return TryReadStackU64(address, out value);
}
value = 0;
if (!IsReadableHostRange(address, sizeof(ulong)))
{
return false;
}
ulong readValue = 0;
if (ReadProcessMemory(
(nint)(-1),
(nint)address,
&readValue,
sizeof(ulong),
out var bytesRead) == 0 ||
bytesRead != sizeof(ulong))
{
return false;
}
value = readValue;
return true;
}
private unsafe static bool TryReadHostBytes(ulong address, byte[] buffer)
{
if (OperatingSystem.IsLinux())
{
fixed (byte* destination = buffer)
{
return TryReadLinuxMemory(address, destination, buffer.Length);
}
}
if (OperatingSystem.IsMacOS())
{
fixed (byte* destination = buffer)
{
return TryReadMacOsMemory(address, destination, buffer.Length);
}
}
if (!IsReadableHostRange(address, buffer.Length))
if (address < 65536)
{
return false;
}
if (buffer.Length == 0)
{
return true;
}
if (OperatingSystem.IsWindows())
{
fixed (byte* destination = buffer)
ulong end = address + (ulong)buffer.Length;
for (ulong page = address & 0xFFFFFFFFFFFFF000uL; page < end; page += 4096)
{
return ReadProcessMemory(
(nint)(-1),
(nint)address,
destination,
(nuint)buffer.Length,
out var bytesRead) != 0 &&
bytesRead == (nuint)buffer.Length;
if (VirtualQuery((void*)page, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
mbi.State != MEM_COMMIT ||
!IsReadableProtection(mbi.Protect))
{
return false;
}
}
}
@ -1403,176 +1465,6 @@ public sealed partial class DirectExecutionBackend
}
}
private unsafe static bool TryReadLinuxMemory(ulong address, byte* destination, int byteCount)
{
if (byteCount < 0 || !IsCanonicalUserAddress(address) ||
address > 0x0000_8000_0000_0000UL - (ulong)byteCount)
{
return false;
}
if (byteCount == 0) return true;
// Read through the kernel so untracked or inaccessible pages cannot fault the reader.
var local = new DiagnosticMemoryVector { Address = (nint)destination, Length = (nuint)byteCount };
var remote = new DiagnosticMemoryVector { Address = (nint)address, Length = (nuint)byteCount };
return ReadLinuxProcessMemory(Environment.ProcessId, &local, 1, &remote, 1, 0) == byteCount;
}
[StructLayout(LayoutKind.Sequential)]
private struct DiagnosticMemoryVector
{
public nint Address;
public nuint Length;
}
[DllImport("libc", EntryPoint = "process_vm_readv")]
private unsafe static extern nint ReadLinuxProcessMemory(
int processId, DiagnosticMemoryVector* local, nuint localCount,
DiagnosticMemoryVector* remote, nuint remoteCount, nuint flags);
private unsafe static bool TryReadMacOsMemory(ulong address, byte* destination, int byteCount)
{
if (byteCount < 0 || !IsCanonicalUserAddress(address) ||
address > 0x0000_8000_0000_0000UL - (ulong)byteCount)
{
return false;
}
if (byteCount == 0)
{
return true;
}
// The host memory table does not contain all guest code mappings.
// Use Mach to read the memory and report access errors without another fault.
return ReadMachVirtualMemory(GetCurrentMachTask(), address, (ulong)byteCount,
(ulong)destination, out var bytesRead) == 0 && bytesRead == (ulong)byteCount;
}
[DllImport("libSystem.B.dylib", EntryPoint = "mach_task_self")]
private static extern uint GetCurrentMachTask();
[DllImport("libSystem.B.dylib", EntryPoint = "mach_vm_read_overwrite")]
private static extern int ReadMachVirtualMemory(
uint taskHandle, ulong sourceAddress, ulong byteCount, ulong destinationAddress, out ulong bytesRead);
private unsafe static bool TryReadExecutableBytes(ulong address, byte[] buffer)
{
if (!OperatingSystem.IsWindows())
{
return TryReadHostBytes(address, buffer);
}
// Faulting x64 code can have execute-only protection. Data probes still require read access.
if (!IsReadableHostRange(address, buffer.Length, allowExecuteOnly: true))
{
return false;
}
if (buffer.Length == 0)
{
return true;
}
// RIP identifies code that Windows was already executing when the
// exception occurred. These mappings are stable for the duration of
// dispatch, so read them directly. Arbitrary diagnostic and operand
// addresses continue to use ReadProcessMemory above because they can
// become invalid while an exception is being reported.
new ReadOnlySpan<byte>((void*)address, buffer.Length).CopyTo(buffer);
return true;
}
/// <summary>
/// Reads guest bytes for a crash report, preferring the guest memory view
/// over a raw host read. Guest code pages are commonly committed
/// PAGE_EXECUTE, which no host read can satisfy.
/// </summary>
private bool TryReadGuestOrHostBytes(ulong address, byte[] buffer)
{
if (_cpuContext is { } context)
{
try
{
if (context.Memory.TryRead(address, buffer))
{
return true;
}
}
catch
{
// Fall through to the host reader.
}
}
return TryReadHostBytes(address, buffer);
}
private unsafe static void DumpHostRegion(string name, ulong address)
{
if (VirtualQuery((void*)address, out var mbi, (nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0)
{
Console.Error.WriteLine($"[LOADER][INFO] {name} region: <VirtualQuery failed> address=0x{address:X16}");
return;
}
Console.Error.WriteLine(
$"[LOADER][INFO] {name} region: base=0x{mbi.BaseAddress:X16} size=0x{mbi.RegionSize:X16} " +
$"state=0x{mbi.State:X08} protect=0x{mbi.Protect:X08} type=0x{mbi.Type:X08}");
}
private unsafe static bool IsReadableHostRange(ulong address, int byteCount, bool allowExecuteOnly = false)
{
if (byteCount < 0 || !IsCanonicalUserAddress(address))
{
return false;
}
var length = (ulong)byteCount;
if (address > 0x0000_8000_0000_0000UL - length)
{
return false;
}
var end = address + length;
var cursor = address;
while (cursor < end)
{
if (VirtualQuery(
(void*)cursor,
out var mbi,
(nuint)sizeof(MEMORY_BASIC_INFORMATION64)) == 0 ||
mbi.State != MEM_COMMIT ||
(!IsReadableProtection(mbi.Protect) &&
!(allowExecuteOnly && mbi.Protect == 0x10)))
{
return false;
}
var regionEnd = mbi.BaseAddress + mbi.RegionSize;
if (regionEnd < mbi.BaseAddress || regionEnd <= cursor)
{
return false;
}
cursor = Math.Min(regionEnd, end);
}
return true;
}
private static bool IsCanonicalUserAddress(ulong address) =>
address >= 0x10000 && address < 0x0000_8000_0000_0000UL;
[DllImport("kernel32.dll", SetLastError = false)]
private unsafe static extern int ReadProcessMemory(
nint process,
nint baseAddress,
void* buffer,
nuint byteCount,
out nuint bytesRead);
private string FormatPointerWithNearestSymbol(ulong value)
{
string text = $"0x{value:X16}";
@ -1586,10 +1478,9 @@ public sealed partial class DirectExecutionBackend
private void InitializeRuntimeSymbolIndex(IReadOnlyDictionary<string, ulong> runtimeSymbols)
{
var symbolsByName = new Dictionary<string, ulong>(StringComparer.Ordinal);
_runtimeSymbolsByName.Clear();
if (runtimeSymbols.Count == 0)
{
Volatile.Write(ref _runtimeSymbolsByName, symbolsByName);
_runtimeSymbolsByAddress = Array.Empty<KeyValuePair<string, ulong>>();
return;
}
@ -1600,12 +1491,11 @@ public sealed partial class DirectExecutionBackend
if (runtimeSymbol.Value != 0L && !string.IsNullOrWhiteSpace(runtimeSymbol.Key))
{
list.Add(runtimeSymbol);
symbolsByName[runtimeSymbol.Key] = runtimeSymbol.Value;
_runtimeSymbolsByName[runtimeSymbol.Key] = runtimeSymbol.Value;
}
}
list.Sort((a, b) => a.Value.CompareTo(b.Value));
Volatile.Write(ref _runtimeSymbolsByName, symbolsByName);
_runtimeSymbolsByAddress = list.ToArray();
}
@ -1911,14 +1801,7 @@ public sealed partial class DirectExecutionBackend
return;
}
var counts = PatchTlsPatternsInRange(committedBase, committedBase + committedSize);
if (counts.Total != 0)
{
Console.Error.WriteLine(
$"[LOADER][INFO] Patched {counts.Loads} TLS loads, {counts.Stores} TLS stores, " +
$"{counts.StackCanaries} stack-canary accesses " +
$"(lazy-commit rescan 0x{committedBase:X16}-0x{committedBase + committedSize:X16})");
}
PatchTlsPatternsInRange(committedBase, committedBase + committedSize, announce: false);
}
private static bool ShouldTraceLazyCommit(int traceIndex)

View File

@ -1,286 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Collections.Generic;
using System.Runtime.InteropServices;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
/// <summary>
/// Native fast-path stubs for the trivial per-thread imports that dominate the
/// HLE call count. Each stub answers from <see cref="GuestFastPath"/>'s
/// per-host-thread block and jumps to the normal import trampoline whenever it
/// cannot, so the managed implementation stays the single source of truth for
/// every case the stub does not handle.
/// </summary>
public sealed unsafe partial class DirectExecutionBackend
{
internal enum GuestFastPathStub
{
None,
PthreadSelf,
PthreadGetspecific,
MemoryCopy,
}
// scePthreadSelf / pthread_self.
private const string PthreadSelfNid = "aI+OeCz8xrQ";
private const string PosixPthreadSelfNid = "EotR8a3ASf4";
// scePthreadGetspecific / pthread_getspecific.
private const string PthreadGetspecificNid = "eoht7mQOCmo";
private const string PosixPthreadGetspecificNid = "0-KXaS70xy4";
// memcpy / memmove.
private const string MemcpyNid = "Q3VBxCXhUHs";
private const string MemmoveNid = "+P6FRGH4LfA";
// Guest memory is identity-mapped and every fault the copy can take (write tracking, lazy
// commit) is resolved from the fault address alone, so the copy may run as guest-side code.
// SHARPEMU_HLE_FAST_MEMCPY=0 keeps it on the managed export.
private static readonly bool GuestMemoryCopyStubEnabled =
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_HLE_FAST_MEMCPY"), "0", StringComparison.Ordinal);
private uint _fastPathBlockTlsIndex = uint.MaxValue;
private bool _guestFastPathEnabled;
private int _guestFastPathStubCount;
/// <summary>
/// Decides whether native fast-path stubs may be installed for this run and
/// arms <see cref="GuestFastPath"/> if so. Any diagnostic mode that must
/// observe every import call keeps the fast path off, because a stub answers
/// without ever reaching the managed gateway that does the tracing.
/// </summary>
private void ConfigureGuestFastPath()
{
_guestFastPathEnabled = false;
_guestFastPathStubCount = 0;
// Emission uses the macOS pthread gs-indexed key array. Windows and
// Linux keep the managed gateway until their TLS read is emitted too.
if (!OperatingSystem.IsMacOS() ||
RuntimeInformation.ProcessArchitecture != Architecture.X64)
{
return;
}
if (string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_HLE_FAST_PATH"), "0", StringComparison.Ordinal))
{
Console.Error.WriteLine("[LOADER][INFO] HLE native fast path disabled by SHARPEMU_HLE_FAST_PATH=0.");
return;
}
if (_logAllImports ||
!string.IsNullOrEmpty(_importFilter) ||
string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_LOG_PTHREADS"), "1", StringComparison.Ordinal))
{
Console.Error.WriteLine(
"[LOADER][INFO] HLE native fast path disabled: import/pthread tracing needs every call in the gateway.");
return;
}
if (_fastPathBlockTlsIndex == uint.MaxValue)
{
_fastPathBlockTlsIndex = TlsAlloc();
if (_fastPathBlockTlsIndex == uint.MaxValue)
{
return;
}
}
var tlsIndex = _fastPathBlockTlsIndex;
GuestFastPath.Enable(block => TlsSetValue(tlsIndex, block));
_guestFastPathEnabled = true;
}
private static GuestFastPathStub ClassifyGuestFastPathStub(string nid) => nid switch
{
PthreadSelfNid or PosixPthreadSelfNid => GuestFastPathStub.PthreadSelf,
PthreadGetspecificNid or PosixPthreadGetspecificNid => GuestFastPathStub.PthreadGetspecific,
MemcpyNid or MemmoveNid => GuestFastPathStub.MemoryCopy,
_ => GuestFastPathStub.None,
};
/// <summary>
/// Builds the native stub for <paramref name="nid"/>, falling back to
/// <paramref name="fallbackTrampoline"/> (the import's managed gateway
/// trampoline) for every case the stub declines.
/// </summary>
private unsafe bool TryCreateGuestFastPathStub(string nid, nint fallbackTrampoline, out nint address)
{
address = 0;
if (!_guestFastPathEnabled || fallbackTrampoline == 0)
{
return false;
}
var kind = ClassifyGuestFastPathStub(nid);
if (kind == GuestFastPathStub.None ||
// The write watch inspects every managed guest write, which a native copy never reaches.
(kind == GuestFastPathStub.MemoryCopy && (!GuestMemoryCopyStubEnabled || GuestWriteWatch.Armed)))
{
return false;
}
var code = EmitGuestFastPathStub(kind, _fastPathBlockTlsIndex, fallbackTrampoline);
const uint stubAllocationSize = 128u;
if (code.Count > stubAllocationSize)
{
return false;
}
void* memory = VirtualAlloc(null, stubAllocationSize, 12288u, 64u);
if (memory == null)
{
return false;
}
for (var i = 0; i < code.Count; i++)
{
((byte*)memory)[i] = code[i];
}
uint oldProtect = 0;
if (!VirtualProtect(memory, stubAllocationSize, 32u, &oldProtect))
{
VirtualFree(memory, 0u, 32768u);
return false;
}
FlushInstructionCache(GetCurrentProcess(), memory, (nuint)code.Count);
address = (nint)memory;
_importHandlerTrampolines.Add(address);
_guestFastPathStubCount++;
return true;
}
/// <summary>
/// Emits one fast-path stub. The stubs obey the SysV ABI the guest calls
/// them with: the answer goes in RAX, only RAX and the arithmetic flags are
/// clobbered (both caller-saved across a call), and on the slow path every
/// register - including the incoming RAX that carries the variadic vector
/// count - is restored before jumping to the trampoline.
/// </summary>
internal static List<byte> EmitGuestFastPathStub(
GuestFastPathStub kind,
uint blockTlsIndex,
nint fallbackTrampoline)
{
if (kind == GuestFastPathStub.MemoryCopy)
{
return EmitGuestMemoryCopyStub(fallbackTrampoline);
}
var code = new List<byte>(48);
var slowPathFixups = new List<int>();
void Emit(params byte[] bytes) => code.AddRange(bytes);
// Emits a short conditional branch to the slow path; 0x74 is JE, 0x73 is JAE.
void EmitBranchToSlowPath(byte opcode)
{
Emit(opcode, 0x00); // rel8, patched once the slow-path offset is known
slowPathFixups.Add(code.Count - 1);
}
void EmitJumpToSlowPath() => EmitBranchToSlowPath(0x74);
// push rax: the guest's incoming RAX/AL must survive a slow-path bail.
Emit(0x50);
// mov rax, gs:[blockTlsIndex * 8] - the macOS pthread key array is the
// gs-based thread specific data array, so a key read is a single load.
Emit(0x65, 0x48, 0x8B, 0x04, 0x25);
code.AddRange(BitConverter.GetBytes(checked(blockTlsIndex * 8u)));
// test rax, rax / je slow: no block published on this host thread.
Emit(0x48, 0x85, 0xC0);
EmitJumpToSlowPath();
switch (kind)
{
case GuestFastPathStub.PthreadSelf:
// cmp qword [rax + SelfHandleOffset], 0 / je slow: no guest
// thread bound here, and a host thread's pthread_self has to
// reach the managed export for its scheduler registration.
Emit(0x48, 0x83, 0x78, checked((byte)GuestFastPath.BlockSelfHandleOffset), 0x00);
EmitJumpToSlowPath();
// inc qword [rax + SelfHitsOffset]
Emit(0x48, 0xFF, 0x40, checked((byte)GuestFastPath.BlockSelfHitsOffset));
// mov rax, [rax + SelfHandleOffset]
Emit(0x48, 0x8B, 0x40, checked((byte)GuestFastPath.BlockSelfHandleOffset));
break;
case GuestFastPathStub.PthreadGetspecific:
// cmp qword [rax + TlsValuesOffset], 0 / je slow: this thread
// has no value table published.
Emit(0x48, 0x83, 0x78, checked((byte)GuestFastPath.BlockTlsValuesOffset), 0x00);
EmitJumpToSlowPath();
// cmp rdi, TlsSlotCount / jae slow. An out-of-range or
// sign-extended negative key is answered by the managed path,
// which truncates it to int and consults the key registry.
Emit(0x48, 0x81, 0xFF);
code.AddRange(BitConverter.GetBytes(GuestFastPath.TlsSlotCount));
EmitBranchToSlowPath(0x73);
// inc qword [rax + GetspecificHitsOffset]
Emit(0x48, 0xFF, 0x40, checked((byte)GuestFastPath.BlockGetspecificHitsOffset));
// mov rax, [rax + TlsValuesOffset] / mov rax, [rax + rdi*8]
Emit(0x48, 0x8B, 0x40, checked((byte)GuestFastPath.BlockTlsValuesOffset));
Emit(0x48, 0x8B, 0x04, 0xF8);
break;
default:
throw new ArgumentOutOfRangeException(nameof(kind), kind, "Unknown fast-path stub kind.");
}
// add rsp, 8 (drop the saved RAX - the result replaces it) / ret
Emit(0x48, 0x83, 0xC4, 0x08);
Emit(0xC3);
var slowPathOffset = code.Count;
// pop rax / jmp qword [rip+0] - the absolute target follows the
// instruction, so nothing else is clobbered on the way out.
Emit(0x58);
Emit(0xFF, 0x25, 0x00, 0x00, 0x00, 0x00);
code.AddRange(BitConverter.GetBytes((long)fallbackTrampoline));
foreach (var fixup in slowPathFixups)
{
var delta = slowPathOffset - (fixup + 1);
if (delta is < 0 or > 127)
{
throw new InvalidOperationException("Fast-path stub slow-path branch is out of rel8 range.");
}
code[fixup] = (byte)delta;
}
return code;
}
/// <summary>
/// memcpy/memmove(dst, src, n) as a forward <c>rep movsb</c>, returning dst. The libc
/// export copies with memmove semantics, so a destination that starts inside the source
/// (where a forward copy would read bytes it already overwrote) takes the managed path.
/// Only RAX, RCX, RSI, RDI and the flags change - all caller-saved - and the direction
/// flag is clear on entry per the SysV ABI.
/// </summary>
internal static List<byte> EmitGuestMemoryCopyStub(nint fallbackTrampoline)
{
var code = new List<byte>(40);
// mov rcx, rdi / sub rcx, rsi / cmp rcx, rdx / jb slow: dst - src < n (unsigned) is
// exactly a destination in (src, src + n) or equal to src with a non-empty copy.
code.AddRange([0x48, 0x89, 0xF9, 0x48, 0x29, 0xF1, 0x48, 0x39, 0xD1, 0x72, 0x00]);
var slowPathFixup = code.Count - 1;
// mov rax, rdi / mov rcx, rdx / rep movsb / ret
code.AddRange([0x48, 0x89, 0xF8, 0x48, 0x89, 0xD1, 0xF3, 0xA4, 0xC3]);
code[slowPathFixup] = checked((byte)(code.Count - (slowPathFixup + 1)));
// jmp qword [rip+0] with the absolute trampoline address after it.
code.AddRange([0xFF, 0x25, 0x00, 0x00, 0x00, 0x00]);
code.AddRange(BitConverter.GetBytes((long)fallbackTrampoline));
return code;
}
}

View File

@ -7,7 +7,6 @@ using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;
using System.Threading;
using SharpEmu.HLE;
namespace SharpEmu.Core.Cpu.Native;
@ -41,9 +40,6 @@ public sealed partial class DirectExecutionBackend
? report
: 15;
private static readonly string? _profileGuestRipThreadFilter =
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_RIP_THREAD");
private const ulong GuestImageBase = 0x0000_0008_0000_0000UL;
private const ulong GuestImageLimit = 0x0000_0009_0000_0000UL;
@ -52,30 +48,31 @@ public sealed partial class DirectExecutionBackend
private readonly ConcurrentDictionary<string, long> _guestRipThreadSamples = new();
private readonly ConcurrentDictionary<string, long> _guestWaitSamples = new();
private readonly ConcurrentDictionary<string, long> _guestThreadWaitSamples = new();
private readonly ConcurrentDictionary<string, long> _guestThreadWaitReasonSamples = new();
private long _guestRipTotalSamples;
private long _guestWaitTotalSamples;
private long _guestRipCaptureFailures;
private long _guestRipSamplerErrors;
private readonly record struct GuestRipSampleTarget(
string Name,
CpuContext Context,
int HostThreadId,
string? BlockReason);
private int _guestRipSampleCursor;
/// <summary>
/// Names the HLE call a thread is parked in, using the guest RIP the import
/// dispatcher left on its context.
/// </summary>
private string ResolveWaitLabel(CpuContext context, string? blockReason)
private string ResolveWaitLabel(GuestThreadState thread)
{
var context = thread.Context;
if (context is null)
{
return "<no-context>";
}
var importIndex = context.ActiveImportIndex;
if ((uint)importIndex >= (uint)_importEntries.Length)
{
// Host code with no import in flight: the thread is parked by the
// emulator's own scheduler. The cooperative block records why, which
// is the part that actually identifies what the frame is waiting on.
var blockReason = thread.BlockReason;
return string.IsNullOrEmpty(blockReason)
? "<idle-or-scheduler>"
: $"blocked:{blockReason}";
@ -119,8 +116,7 @@ public sealed partial class DirectExecutionBackend
sampler.Start();
Console.Error.WriteLine(
$"[PERF][GUEST] RIP sampler started: interval={_profileGuestRipIntervalMs}ms " +
$"report={_profileGuestRipReportSeconds}s " +
$"thread={_profileGuestRipThreadFilter ?? "<all>"}");
$"report={_profileGuestRipReportSeconds}s");
}
private void GuestRipSampleLoop()
@ -133,20 +129,14 @@ public sealed partial class DirectExecutionBackend
{
try
{
var guestThreads = SnapshotGuestRipTargets();
if (!string.IsNullOrWhiteSpace(_profileGuestRipThreadFilter))
var guestThreads = SnapshotGuestThreads();
var sampleIndex = guestThreads.Length == 0
? 0
: (int)((uint)Interlocked.Increment(ref _guestRipSampleCursor) % (uint)guestThreads.Length);
foreach (var thread in guestThreads.Skip(sampleIndex).Take(1))
{
guestThreads = guestThreads
.Where(thread => thread.Name.Contains(
_profileGuestRipThreadFilter,
StringComparison.OrdinalIgnoreCase))
.ToArray();
}
var sampledHostThreads = new HashSet<int>();
foreach (var thread in guestThreads)
{
var hostThreadId = thread.HostThreadId;
if (hostThreadId == 0 || !sampledHostThreads.Add(hostThreadId))
var hostThreadId = Volatile.Read(ref thread.HostThreadId);
if (hostThreadId == 0)
{
continue;
}
@ -175,18 +165,12 @@ public sealed partial class DirectExecutionBackend
continue;
}
var threadName = string.IsNullOrEmpty(thread.Name) ? "<unnamed>" : thread.Name;
var waitLabel = ResolveWaitLabel(thread.Context, thread.BlockReason);
_guestWaitSamples.AddOrUpdate(
waitLabel,
ResolveWaitLabel(thread),
1,
static (_, value) => value + 1);
_guestThreadWaitSamples.AddOrUpdate(
threadName,
1,
static (_, value) => value + 1);
_guestThreadWaitReasonSamples.AddOrUpdate(
$"{threadName}\u001F{waitLabel}",
string.IsNullOrEmpty(thread.Name) ? "<unnamed>" : thread.Name,
1,
static (_, value) => value + 1);
Interlocked.Increment(ref _guestWaitTotalSamples);
@ -217,38 +201,10 @@ public sealed partial class DirectExecutionBackend
}
}
private GuestRipSampleTarget[] SnapshotGuestRipTargets()
{
using (LockGate("SnapshotGuestRipTargets"))
{
var targets = new GuestRipSampleTarget[_guestThreads.Count + _externalGuestThreads.Count];
var index = 0;
foreach (var thread in _guestThreads.Values)
{
targets[index++] = new GuestRipSampleTarget(
thread.Name,
thread.Context,
Volatile.Read(ref thread.HostThreadId),
thread.BlockReason);
}
foreach (var pair in _externalGuestThreads)
{
var thread = pair.Value;
targets[index++] = new GuestRipSampleTarget(
thread.Name,
thread.Context,
Volatile.Read(ref thread.HostThreadId),
null);
}
return targets;
}
}
private void ReportGuestRipSamples(long windowSamples, double windowSeconds)
{
if (windowSamples == 0)
var total = Interlocked.Read(ref _guestRipTotalSamples);
if (total == 0)
{
return;
}
@ -277,7 +233,7 @@ public sealed partial class DirectExecutionBackend
}
Console.Error.WriteLine(
$"[PERF][GUEST] window={windowSamples} in {windowSeconds:F1}s " +
$"[PERF][GUEST] samples={total} window={windowSamples} in {windowSeconds:F1}s " +
$"capture_failures={Interlocked.Read(ref _guestRipCaptureFailures)}");
Console.Error.WriteLine(
@ -287,7 +243,7 @@ public sealed partial class DirectExecutionBackend
byRip.OrderByDescending(pair => pair.Value)
.Take(12)
.Select(pair =>
$"0x{pair.Key:X}{DescribeGuestAddress(pair.Key)}={pair.Value * 100.0 / windowSamples:F1}%")));
$"0x{pair.Key:X}{DescribeGuestAddress(pair.Key)}={pair.Value * 100.0 / total:F1}%")));
Console.Error.WriteLine(
"[PERF][GUEST] top_page: " +
@ -296,7 +252,7 @@ public sealed partial class DirectExecutionBackend
byPage.OrderByDescending(pair => pair.Value)
.Take(8)
.Select(pair =>
$"0x{pair.Key:X}{DescribeGuestAddress(pair.Key)}={pair.Value * 100.0 / windowSamples:F1}%")));
$"0x{pair.Key:X}{DescribeGuestAddress(pair.Key)}={pair.Value * 100.0 / total:F1}%")));
var byWait = new List<KeyValuePair<string, long>>(_guestWaitSamples.Count + 16);
foreach (var pair in _guestWaitSamples)
@ -306,26 +262,12 @@ public sealed partial class DirectExecutionBackend
var waitTotal = Interlocked.Read(ref _guestWaitTotalSamples);
Console.Error.WriteLine(
$"[PERF][GUEST] waiting={waitTotal * 100.0 / windowSamples:F1}% of guest thread-time; top_wait: " +
$"[PERF][GUEST] waiting={waitTotal * 100.0 / total:F1}% of guest thread-time; top_wait: " +
string.Join(
" | ",
byWait.OrderByDescending(pair => pair.Value)
.Take(12)
.Select(pair => $"{pair.Key}={pair.Value * 100.0 / windowSamples:F1}%")));
Console.Error.WriteLine(
"[PERF][GUEST] thread_wait: " +
string.Join(
" | ",
_guestThreadWaitReasonSamples.OrderByDescending(pair => pair.Value)
.Take(16)
.Select(pair =>
{
var separator = pair.Key.IndexOf('\u001F');
var threadName = separator >= 0 ? pair.Key[..separator] : pair.Key;
var waitLabel = separator >= 0 ? pair.Key[(separator + 1)..] : "<unknown>";
return $"{threadName}->{waitLabel}={pair.Value * 100.0 / windowSamples:F1}%";
})));
.Select(pair => $"{pair.Key}={pair.Value * 100.0 / total:F1}%")));
// Per-thread spin/park split. The global wait share mixes the job pool in
// with a dozen dormant threads, which hides the number that matters:
@ -349,14 +291,7 @@ public sealed partial class DirectExecutionBackend
" | ",
byThread.OrderByDescending(pair => pair.Value)
.Take(10)
.Select(pair => $"{pair.Key}={pair.Value * 100.0 / windowSamples:F1}%")));
_guestRipSamples.Clear();
_guestRipThreadSamples.Clear();
_guestWaitSamples.Clear();
_guestThreadWaitSamples.Clear();
_guestThreadWaitReasonSamples.Clear();
Interlocked.Exchange(ref _guestWaitTotalSamples, 0);
.Select(pair => $"{pair.Key}={pair.Value * 100.0 / total:F1}%")));
}
/// <summary>

View File

@ -1,309 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Collections.Concurrent;
using System.Diagnostics;
namespace SharpEmu.Core.Cpu.Native;
public sealed unsafe partial class DirectExecutionBackend
{
private sealed class GuestSchedulerThreadStats
{
public string Name = string.Empty;
public long ReadyTimestamp;
public long BlockTimestamp;
public long ScheduledTimestamp;
public long RunTimestamp;
public long ReadyCount;
public long ClaimCount;
public long ScheduleCount;
public long RunCount;
public long BlockCount;
public long ResumeCount;
public long ReadyTicks;
public long BlockTicks;
public long ScheduleTicks;
public long RunTicks;
public long MaxReadyTicks;
public long MaxBlockTicks;
public long MaxScheduleTicks;
public long MaxRunTicks;
public string? LastBlockReason;
public int Completed;
}
private static readonly bool _profileGuestScheduler =
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_SCHEDULER"),
"1",
StringComparison.Ordinal) ||
string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE"),
"1",
StringComparison.Ordinal);
private readonly ConcurrentDictionary<ulong, GuestSchedulerThreadStats> _guestSchedulerStats = new();
private readonly GuestThreadFlowProfile _guestThreadFlowProfile = new();
private readonly object _guestTraceOutputGate = new();
private long _guestSchedulerProfileWindowStart = Stopwatch.GetTimestamp();
private long _guestSchedulerProfileNextReport =
Stopwatch.GetTimestamp() + (Stopwatch.Frequency * 5L);
private int _guestSchedulerProfileReporting;
private void StartGuestFlowTraces()
{
lock (_guestTraceOutputGate)
{
_guestThreadFlowProfile.StartSession();
SharpEmu.Libs.Diagnostics.SemaphoreSignalProfile.StartSession();
SharpEmu.Libs.Diagnostics.MutexHandoffProfile.StartSession();
SharpEmu.Libs.VideoOut.SubmissionFlowProfile.StartSession();
}
}
private void WriteGuestFlowTraces(TextWriter? output = null)
{
output ??= Console.Error;
lock (_guestTraceOutputGate)
{
var signals = SharpEmu.Libs.Diagnostics.SemaphoreSignalProfile.Close();
var mutexes = SharpEmu.Libs.Diagnostics.MutexHandoffProfile.Close();
SharpEmu.Libs.VideoOut.SubmissionFlowProfile.WriteTrace(output);
try
{
_guestThreadFlowProfile.WriteTrace(output);
SharpEmu.Libs.Diagnostics.SemaphoreSignalProfile.WriteTrace(output, signals);
SharpEmu.Libs.Diagnostics.MutexHandoffProfile.WriteTrace(output, mutexes);
}
catch (Exception exception) when (exception is IOException or ObjectDisposedException)
{
// A closed trace output must not prevent native session cleanup.
}
}
}
private void RecordGuestThreadFlow(GuestThreadState thread, GuestThreadFlowProfile.EventKind kind, string? reason = null)
{
if (!GuestThreadFlowProfile.Enabled) return;
_guestThreadFlowProfile.Record(new GuestThreadFlowProfile.TraceEvent(
Stopwatch.GetTimestamp(), Environment.CurrentManagedThreadId, thread.ThreadHandle,
thread.Name, kind, reason, thread.BlockWakeKey, Volatile.Read(ref thread.LastImportNid),
Volatile.Read(ref thread.LastImportRdi), Volatile.Read(ref thread.LastImportRsi), thread.BlockDeadlineTimestamp));
}
private GuestSchedulerThreadStats GetGuestSchedulerStats(GuestThreadState thread)
{
var stats = _guestSchedulerStats.GetOrAdd(
thread.ThreadHandle,
static _ => new GuestSchedulerThreadStats());
stats.Name = thread.Name;
return stats;
}
private void ProfileGuestThreadReady(GuestThreadState thread)
{
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.Ready);
if (!_profileGuestScheduler)
{
return;
}
var stats = GetGuestSchedulerStats(thread);
var now = Stopwatch.GetTimestamp();
Volatile.Write(ref stats.ReadyTimestamp, now);
Interlocked.Increment(ref stats.ReadyCount);
var blockTimestamp = Interlocked.Exchange(ref stats.BlockTimestamp, 0);
if (blockTimestamp != 0 && now >= blockTimestamp)
{
var ticks = now - blockTimestamp;
Interlocked.Increment(ref stats.ResumeCount);
Interlocked.Add(ref stats.BlockTicks, ticks);
UpdateGuestSchedulerMaximum(ref stats.MaxBlockTicks, ticks);
}
}
private void ProfileGuestThreadClaimed(GuestThreadState thread)
{
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.Claimed);
if (!_profileGuestScheduler)
{
return;
}
var now = Stopwatch.GetTimestamp();
var stats = GetGuestSchedulerStats(thread);
var readyTimestamp = Interlocked.Exchange(ref stats.ReadyTimestamp, 0);
Interlocked.Increment(ref stats.ClaimCount);
if (readyTimestamp != 0 && now >= readyTimestamp)
{
var ticks = now - readyTimestamp;
Interlocked.Add(ref stats.ReadyTicks, ticks);
UpdateGuestSchedulerMaximum(ref stats.MaxReadyTicks, ticks);
}
}
private void ProfileGuestThreadScheduled(GuestThreadState thread)
{
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.Scheduled);
if (!_profileGuestScheduler)
{
return;
}
var stats = GetGuestSchedulerStats(thread);
Volatile.Write(ref stats.ScheduledTimestamp, Stopwatch.GetTimestamp());
Interlocked.Increment(ref stats.ScheduleCount);
}
private void ProfileGuestThreadRunStarted(GuestThreadState thread)
{
RecordGuestThreadFlow(thread, GuestThreadFlowProfile.EventKind.RunStarted);
if (!_profileGuestScheduler)
{
return;
}
var now = Stopwatch.GetTimestamp();
var stats = GetGuestSchedulerStats(thread);
var scheduledTimestamp = Interlocked.Exchange(ref stats.ScheduledTimestamp, 0);
Volatile.Write(ref stats.RunTimestamp, now);
Interlocked.Increment(ref stats.RunCount);
if (scheduledTimestamp != 0 && now >= scheduledTimestamp)
{
var ticks = now - scheduledTimestamp;
Interlocked.Add(ref stats.ScheduleTicks, ticks);
UpdateGuestSchedulerMaximum(ref stats.MaxScheduleTicks, ticks);
}
}
private void ProfileGuestThreadRunStopped(GuestThreadState thread)
{
if (!_profileGuestScheduler)
{
return;
}
var now = Stopwatch.GetTimestamp();
var stats = GetGuestSchedulerStats(thread);
var runTimestamp = Interlocked.Exchange(ref stats.RunTimestamp, 0);
if (runTimestamp != 0 && now >= runTimestamp)
{
var ticks = now - runTimestamp;
Interlocked.Add(ref stats.RunTicks, ticks);
UpdateGuestSchedulerMaximum(ref stats.MaxRunTicks, ticks);
}
if (thread.State == GuestThreadRunState.Blocked)
{
Interlocked.Increment(ref stats.BlockCount);
Volatile.Write(ref stats.BlockTimestamp, now);
Volatile.Write(ref stats.LastBlockReason, thread.BlockReason);
}
else if (thread.State is GuestThreadRunState.Exited or GuestThreadRunState.Faulted)
{
Volatile.Write(ref stats.Completed, 1);
}
TryReportGuestSchedulerProfile(now);
}
private void TryReportGuestSchedulerProfile(long now)
{
var deadline = Volatile.Read(ref _guestSchedulerProfileNextReport);
if (now < deadline ||
Interlocked.CompareExchange(ref _guestSchedulerProfileReporting, 1, 0) != 0)
{
return;
}
try
{
Volatile.Write(
ref _guestSchedulerProfileNextReport,
now + (Stopwatch.Frequency * 5L));
var windowStart = Interlocked.Exchange(ref _guestSchedulerProfileWindowStart, now);
var seconds = Math.Max(
(now - windowStart) / (double)Stopwatch.Frequency,
double.Epsilon);
var rows = new List<(long Rank, string Text)>();
foreach (var pair in _guestSchedulerStats)
{
var stats = pair.Value;
var readyCount = Interlocked.Exchange(ref stats.ReadyCount, 0);
var claimCount = Interlocked.Exchange(ref stats.ClaimCount, 0);
var scheduleCount = Interlocked.Exchange(ref stats.ScheduleCount, 0);
var runCount = Interlocked.Exchange(ref stats.RunCount, 0);
var blockCount = Interlocked.Exchange(ref stats.BlockCount, 0);
var resumeCount = Interlocked.Exchange(ref stats.ResumeCount, 0);
var readyTicks = Interlocked.Exchange(ref stats.ReadyTicks, 0);
var blockTicks = Interlocked.Exchange(ref stats.BlockTicks, 0);
var scheduleTicks = Interlocked.Exchange(ref stats.ScheduleTicks, 0);
var runTicks = Interlocked.Exchange(ref stats.RunTicks, 0);
var maxReadyTicks = Interlocked.Exchange(ref stats.MaxReadyTicks, 0);
var maxBlockTicks = Interlocked.Exchange(ref stats.MaxBlockTicks, 0);
var maxScheduleTicks = Interlocked.Exchange(ref stats.MaxScheduleTicks, 0);
var maxRunTicks = Interlocked.Exchange(ref stats.MaxRunTicks, 0);
if (readyCount == 0 && claimCount == 0 && scheduleCount == 0 && runCount == 0)
{
continue;
}
rows.Add((Math.Max(blockTicks, Math.Max(readyTicks, runTicks)),
$"name='{stats.Name}' handle=0x{pair.Key:X16} " +
$"ready={readyCount} claimed={claimCount} scheduled={scheduleCount} " +
$"runs={runCount} blocked={blockCount} resumed={resumeCount} " +
$"blocked_ms={GuestSchedulerMilliseconds(blockTicks):F2} " +
$"blocked_avg_ms={(resumeCount == 0 ? 0.0 : GuestSchedulerMilliseconds(blockTicks) / resumeCount):F3} " +
$"blocked_max_ms={GuestSchedulerMilliseconds(maxBlockTicks):F3} " +
$"ready_ms={GuestSchedulerMilliseconds(readyTicks):F2} " +
$"ready_avg_ms={(claimCount == 0 ? 0.0 : GuestSchedulerMilliseconds(readyTicks) / claimCount):F3} " +
$"ready_max_ms={GuestSchedulerMilliseconds(maxReadyTicks):F3} " +
$"dispatch_ms={GuestSchedulerMilliseconds(scheduleTicks):F2} " +
$"dispatch_avg_ms={(runCount == 0 ? 0.0 : GuestSchedulerMilliseconds(scheduleTicks) / runCount):F3} " +
$"dispatch_max_ms={GuestSchedulerMilliseconds(maxScheduleTicks):F3} " +
$"run_ms={GuestSchedulerMilliseconds(runTicks):F2} " +
$"run_avg_ms={(runCount == 0 ? 0.0 : GuestSchedulerMilliseconds(runTicks) / runCount):F3} " +
$"run_max_ms={GuestSchedulerMilliseconds(maxRunTicks):F3} " +
$"last_block={Volatile.Read(ref stats.LastBlockReason) ?? "none"}"));
if (Volatile.Read(ref stats.Completed) != 0)
{
_guestSchedulerStats.TryRemove(pair.Key, out _);
}
}
Console.Error.WriteLine(
$"[PERF][GUEST_SCHED] window={seconds:F1}s active_threads={rows.Count}");
foreach (var row in rows
.OrderByDescending(static item => item.Rank)
.Take(32))
{
Console.Error.WriteLine($"[PERF][GUEST_SCHED] {row.Text}");
}
}
finally
{
Volatile.Write(ref _guestSchedulerProfileReporting, 0);
}
}
private static double GuestSchedulerMilliseconds(long ticks) =>
ticks * 1000.0 / Stopwatch.Frequency;
private static void UpdateGuestSchedulerMaximum(ref long target, long candidate)
{
var current = Volatile.Read(ref target);
while (candidate > current)
{
var observed = Interlocked.CompareExchange(ref target, candidate, current);
if (observed == current)
{
return;
}
current = observed;
}
}
}

View File

@ -201,7 +201,7 @@ public sealed partial class DirectExecutionBackend
foreach (var attempt in DecodeWindowSizes)
{
var buffer = new byte[attempt];
if (!TryReadExecutableBytes(rip, buffer))
if (!TryReadHostBytes(rip, buffer))
{
continue;
}

View File

@ -72,9 +72,45 @@ public sealed partial class DirectExecutionBackend
private unsafe static int RawVectoredHandlerManaged(void* exceptionInfo)
{
if (TryHandleGuestImageWriteFault(exceptionInfo))
{
return -1;
}
return TryRecoverUnresolvedSentinel(exceptionInfo);
}
/// <summary>
/// Windows counterpart of the POSIX SIGSEGV bridge into
/// <see cref="SharpEmu.HLE.GuestImageWriteTracker"/>. Guest code runs natively,
/// so a store into a surface the GPU backend has cached is an ordinary CPU
/// write with nothing to intercept — the page is write-protected instead and
/// the resulting fault is what tells the backend to re-upload. Without this
/// the cache serves the first upload forever, and anything the guest CPU
/// draws (a software-decoded movie frame, a memset fog layer) never reaches
/// the screen.
/// </summary>
private unsafe static bool TryHandleGuestImageWriteFault(void* exceptionInfo)
{
if (!SharpEmu.HLE.GuestImageWriteTracker.Enabled)
{
return false;
}
var exceptionRecord = ((EXCEPTION_POINTERS*)exceptionInfo)->ExceptionRecord;
// STATUS_ACCESS_VIOLATION, and only the write flavour: ExceptionInformation
// is [accessKind, address] with 0=read, 1=write, 8=DEP execute.
if (exceptionRecord->ExceptionCode != 3221225477u ||
exceptionRecord->NumberParameters < 2 ||
exceptionRecord->ExceptionInformation[0] != 1uL)
{
return false;
}
return SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
exceptionRecord->ExceptionInformation[1]);
}
private unsafe static int RawUnhandledFilterManaged(void* exceptionInfo)
{
return TryRecoverUnresolvedSentinel(exceptionInfo);
@ -164,7 +200,6 @@ public sealed partial class DirectExecutionBackend
return 18446744071562199042uL;
}
ImportStubEntry importStubEntry = _importEntries[importIndex];
using var registerPacketImport = SharpEmu.Libs.Diagnostics.AgcRegisterPacketProfile.MeasureImport(importStubEntry.Nid);
if (_perfHleHistogram)
{
RecordPerfHleCall(importStubEntry.Export?.Name ?? importStubEntry.Nid);
@ -185,11 +220,6 @@ public sealed partial class DirectExecutionBackend
return hotMemoryResult;
}
if (importStubEntry.IsTrivialLeaf && !_disableTrivialLeafDispatch)
{
return DispatchTrivialLeaf(cpuContext, in importStubEntry, argPackPtr, num);
}
if (importStubEntry.IsLeaf &&
TryDispatchLeafImport(cpuContext, importStubEntry, argPackPtr, num, out var leafResult))
{
@ -215,13 +245,6 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
cpuContext.SetImportStackArguments(
ReadImportStackArgument(argPackPtr, 0),
ReadImportStackArgument(argPackPtr, 1),
ReadImportStackArgument(argPackPtr, 2),
ReadImportStackArgument(argPackPtr, 3),
ReadImportStackArgument(argPackPtr, 4),
ReadImportStackArgument(argPackPtr, 5));
ulong value = cpuContext[CpuRegister.Rdi];
ulong value2 = cpuContext[CpuRegister.Rsi];
ulong num3 = cpuContext[CpuRegister.Rdx];
@ -243,8 +266,8 @@ public sealed partial class DirectExecutionBackend
if (probeTarget &&
Interlocked.Increment(ref _probeImportReturnAddressCount) <= 2048)
{
var frameValue = TryReadImportStackU64(value4, out var savedRbp) ? savedRbp : 0;
var frameReturn = TryReadImportStackU64(value4 + sizeof(ulong), out var savedReturn)
var frameValue = TryReadStackU64(value4, out var savedRbp) ? savedRbp : 0;
var frameReturn = TryReadStackU64(value4 + sizeof(ulong), out var savedReturn)
? savedReturn
: 0;
Console.Error.WriteLine(
@ -345,14 +368,6 @@ public sealed partial class DirectExecutionBackend
Console.Error.WriteLine(
$"[LOADER][TRACE] bootstrap_call#{num}: op=0x{value:X16} sym_ptr=0x{value2:X16} sym='{symbolText}' out_ptr=0x{num3:X16} ret=0x{num7:X16}");
}
// Once the host is shutting down, a guest thread still running reaches no more HLE: the
// services behind the imports are being torn down, and a title that sees them fail (a null
// command buffer from the GPU library, say) crashes the process on its way out.
if (_forcedGuestExit && !ActiveForcedGuestExit && TryEndGuestSliceForShutdown(argPackPtr))
{
cpuContext[CpuRegister.Rax] = 1uL;
return 1uL;
}
if (!isGuestWorker &&
!ActiveForcedGuestExit &&
ShouldForceGuestExitOnImportLoop(in importStubEntry, num7, num, value, value2))
@ -1230,56 +1245,10 @@ public sealed partial class DirectExecutionBackend
}
}
// Every import reads its stack arguments. On POSIX a region query takes the global region lock,
// so keep the last readable range per thread until any mapping changes. The signal handlers
// use TryReadStackU64 instead: they must not touch thread-static storage.
[ThreadStatic] private static ulong _importReadableStart;
[ThreadStatic] private static ulong _importReadableEnd;
[ThreadStatic] private static long _importReadableGeneration;
private unsafe static bool TryReadImportStackU64(ulong address, out ulong value)
{
if (OperatingSystem.IsWindows())
{
return TryReadHostQword(address, out value);
}
// Stack arguments sit on the calling guest thread's own stack, which the backend mapped read-write
// for the thread's lifetime and no tracker ever protects. Reading inside it needs no query: the
// cached range below is keyed by a mapping generation that every tracker protection change bumps,
// so falling through here cost a locked host region query per argument on nearly every import.
if (_activeGuestThreadState is { StackSize: > 0 } thread &&
address >= thread.StackBase && address <= thread.StackBase + thread.StackSize - sizeof(ulong))
{
value = *(ulong*)address;
return true;
}
var generation = HostMemory.MappingGeneration;
if (generation == _importReadableGeneration &&
address >= _importReadableStart && address <= _importReadableEnd - 8)
{
value = *(ulong*)address;
return true;
}
if (!TryQueryReadableRange(address, out var start, out var end))
{
value = 0;
return false;
}
_importReadableStart = start;
_importReadableEnd = end;
_importReadableGeneration = generation;
value = *(ulong*)address;
return true;
}
private static ulong ReadImportStackArgument(nint argPackPtr, int index)
{
var address = checked((ulong)argPackPtr + 104UL + (ulong)index * sizeof(ulong));
return TryReadImportStackU64(address, out var value) ? value : 0;
return TryReadHostQword(address, out var value) ? value : 0;
}
private static GuestCpuContinuation CaptureImportBoundaryContinuation(
@ -1390,13 +1359,6 @@ public sealed partial class DirectExecutionBackend
cpuContext[CpuRegister.R14] = *(ulong*)(argPackPtr + 80);
cpuContext[CpuRegister.R15] = *(ulong*)(argPackPtr + 88);
cpuContext[CpuRegister.Rsp] = (ulong)argPackPtr + 96uL;
cpuContext.SetImportStackArguments(
ReadImportStackArgument(argPackPtr, 0),
ReadImportStackArgument(argPackPtr, 1),
ReadImportStackArgument(argPackPtr, 2),
ReadImportStackArgument(argPackPtr, 3),
ReadImportStackArgument(argPackPtr, 4),
ReadImportStackArgument(argPackPtr, 5));
if (_activeGuestThreadState is { } activeGuestThreadState)
{
@ -1518,68 +1480,6 @@ public sealed partial class DirectExecutionBackend
return true;
}
private static readonly bool _disableTrivialLeafDispatch = string.Equals(
Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_TRIVIAL_LEAF"), "1", StringComparison.Ordinal);
// Tiny exports called millions of times per second, where the ~250ns of full
// import bookkeeping costs more than the export itself. Each one reads only
// argument registers (RDI..R9) and thread-local state, returns in RAX, never
// blocks, yields, calls back into the guest or reads the guest stack.
private static bool IsTrivialLeafImport(string nid) =>
nid is
"0-KXaS70xy4" or // pthread_getspecific
"eoht7mQOCmo" or // scePthreadGetspecific
"EI-5-jlq2dE" or // scePthreadGetthreadid
"3eqs37G74-s" or // pthread_getthreadid_np
"BNowx2l588E" or // sceKernelGetProcessTimeCounterFrequency
"fgxnMeTNUtY" or // sceKernelGetProcessTimeCounter
"4J2sUJmuHZQ" or // sceKernelGetProcessTime
"-2IRUCO--PM" or // sceKernelReadTsc
"1j3S3n-tTW4" or // sceKernelGetTscFrequency
"ob5xAW4ln-0" or // strchr
"9yDWMxEFdJU" or // strrchr
"V++UgBtQhn0"; // sceAgcGetDataPacketPayloadAddress
private unsafe ulong DispatchTrivialLeaf(
CpuContext cpuContext,
in ImportStubEntry importStubEntry,
nint argPackPtr,
long dispatchIndex)
{
// Polling loops on the host main thread use the time queries as loop-guard
// boundaries; keep resetting the pattern so skipping the guard here cannot
// turn a legitimate time-polling loop into a forced exit.
if (importStubEntry.IsLoopGuardBoundary && !GuestThreadExecution.IsGuestThread)
{
ResetImportLoopPattern();
}
cpuContext.Rip = importStubEntry.Address;
cpuContext[CpuRegister.Rdi] = *(ulong*)argPackPtr;
cpuContext[CpuRegister.Rsi] = *(ulong*)(argPackPtr + 8);
cpuContext[CpuRegister.Rdx] = *(ulong*)(argPackPtr + 16);
cpuContext[CpuRegister.Rcx] = *(ulong*)(argPackPtr + 24);
cpuContext[CpuRegister.R8] = *(ulong*)(argPackPtr + 32);
cpuContext[CpuRegister.R9] = *(ulong*)(argPackPtr + 40);
cpuContext.ClearRaxWriteFlag();
var returnValue = importStubEntry.Export!.Function(cpuContext);
if (!cpuContext.WasRaxWritten)
{
cpuContext[CpuRegister.Rax] = unchecked((ulong)returnValue);
}
if (returnValue != (int)OrbisGen2Result.ORBIS_GEN2_OK &&
ShouldLogImportResult(importStubEntry.Nid, (OrbisGen2Result)returnValue))
{
Console.Error.WriteLine(
$"[LOADER][WARN] Import#{dispatchIndex} result: {(OrbisGen2Result)returnValue} ({importStubEntry.Nid}) " +
$"rdi=0x{cpuContext[CpuRegister.Rdi]:X16} rsi=0x{cpuContext[CpuRegister.Rsi]:X16} " +
$"ret=0x{*(ulong*)(argPackPtr + 96):X16}");
}
return cpuContext[CpuRegister.Rax];
}
private static bool IsNoBlockLeafImport(string nid) =>
nid is
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
@ -1637,19 +1537,6 @@ public sealed partial class DirectExecutionBackend
var expectedEqueueTimeout =
string.Equals(nid, "fzyMKs9kim0", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
// scePthreadCondTimedwait and sceKernelWaitEventFlag report an elapsed timeout.
var expectedWaitTimeout =
(nid is "BmMjYxmew1w" or "JTvBflhYazQ") &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_TIMED_OUT;
// scePthreadMutexLock on an error-checking mutex the caller already owns; titles use it
// to build their own recursive locks.
var expectedErrorCheckRelock =
string.Equals(nid, "9UK1vLZQft4", StringComparison.Ordinal) &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_DEADLOCK;
// scePadReadState on a handle that is not open; titles poll every pad slot.
var expectedPadNotOpen =
string.Equals(nid, "YndgXqQVV7c", StringComparison.Ordinal) &&
resultValue == unchecked((int)0x80920003);
var expectedMutexTrylockBusy =
(nid is "K-jXhbt2gn4" or "upoVrzMHFeE") &&
result == OrbisGen2Result.ORBIS_GEN2_ERROR_BUSY;
@ -1674,9 +1561,6 @@ public sealed partial class DirectExecutionBackend
if (!expectedFileProbeMiss &&
!expectedTimedWaitTimeout &&
!expectedEqueueTimeout &&
!expectedWaitTimeout &&
!expectedErrorCheckRelock &&
!expectedPadNotOpen &&
!expectedMutexTrylockBusy &&
!expectedSemaphoreTrywaitAgain &&
!expectedPollSemaBusy &&
@ -1735,7 +1619,7 @@ public sealed partial class DirectExecutionBackend
"8aI7R7WaOlc" or // sceAmprCommandBufferConstructor
"zgXifHT9ErY" or // sceVideoOutIsFlipPending
"V++UgBtQhn0" or // sceAgcGetDataPacketPayloadAddress
"qj7QZpgr9Uw" or // Graphics context-state operation
"qj7QZpgr9Uw" or // Gen5 graphics type-2 packet
"LtTouSCZjHM" or // sceAgcCbNop
"k3GhuSNmBLU" or // sceAgcCbDispatch
"UZbQjYAwwXM" or // sceAgcCbSetShRegistersDirect
@ -1869,20 +1753,6 @@ public sealed partial class DirectExecutionBackend
}
}
// Returns the import straight to the host entry stub, which ends this guest slice.
private unsafe bool TryEndGuestSliceForShutdown(nint argPackPtr)
{
ulong sentinel = ActiveEntryReturnSentinelRip;
if (sentinel < 65536 || !TryPatchActiveGuestReturnSlot(sentinel))
{
return false;
}
*(ulong*)(argPackPtr + 96) = sentinel;
ActiveForcedGuestExit = true;
return true;
}
private unsafe bool TryForceGuestExitToHostStub(nint argPackPtr, long dispatchIndex, ulong returnRip, string nid)
{
ulong num = ActiveEntryReturnSentinelRip;
@ -2013,18 +1883,10 @@ public sealed partial class DirectExecutionBackend
private static bool IsImportLoopGuardBoundary(string nid) =>
nid is
"1jfXLRVzisc" or // sceKernelUsleep
"1j3S3n-tTW4" or // sceKernelGetTscFrequency (polling/time base query)
"WKAXJ4XBPQ4" or // scePthreadCondWait
"BmMjYxmew1w" or // scePthreadCondTimedwait
"Op8TBGY5KHg" or // pthread_cond_wait
"27bAgiJmOh0" or // pthread_cond_timedwait
"Zxa0VhQVTsk" or // sceKernelWaitSema
"n88vx3C5nW8" or // gettimeofday
"lLMT9vJAck0" or // clock_gettime
"-2IRUCO--PM" or // sceKernelReadTsc
"4J2sUJmuHZQ" or // sceKernelGetProcessTime
"fgxnMeTNUtY" or // sceKernelGetProcessTimeCounter
"yH17Q6NWtVg"; // sceUserServiceGetEvent (non-blocking event poll)
"27bAgiJmOh0"; // pthread_cond_timedwait
private void ResetImportLoopPattern()
{
@ -2423,18 +2285,17 @@ public sealed partial class DirectExecutionBackend
{
return false;
}
var runtimeSymbolsByName = Volatile.Read(ref _runtimeSymbolsByName);
if (runtimeSymbolsByName.TryGetValue(symbolName, out var value) && IsRuntimeSymbolAddressUsable(value))
if (_runtimeSymbolsByName.TryGetValue(symbolName, out var value) && IsRuntimeSymbolAddressUsable(value))
{
address = value;
return true;
}
if (symbolName.StartsWith("_", StringComparison.Ordinal) && runtimeSymbolsByName.TryGetValue(symbolName[1..], out value) && IsRuntimeSymbolAddressUsable(value))
if (symbolName.StartsWith("_", StringComparison.Ordinal) && _runtimeSymbolsByName.TryGetValue(symbolName[1..], out value) && IsRuntimeSymbolAddressUsable(value))
{
address = value;
return true;
}
if (runtimeSymbolsByName.TryGetValue("_" + symbolName, out value) && IsRuntimeSymbolAddressUsable(value))
if (_runtimeSymbolsByName.TryGetValue("_" + symbolName, out value) && IsRuntimeSymbolAddressUsable(value))
{
address = value;
return true;

View File

@ -1,51 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Runtime.InteropServices;
namespace SharpEmu.Core.Cpu.Native;
// On macOS the runtime receives EXC_BAD_ACCESS on each of its threads through a Mach exception
// port before the kernel raises SIGSEGV or SIGBUS. It calls any fault in the page holding RSP, or
// the page below it, a stack overflow and aborts, assuming RSP is on a stack it owns. Guest code
// runs on guest stacks, and those pages fault whenever GPU or image write tracking has protected
// them, so a thread running guest code must leave its access faults to the signal handler. The
// handler resolves guest faults and chains every other one to the runtime's own signal handler,
// the same path the runtime uses on Linux.
public sealed unsafe partial class DirectExecutionBackend
{
private const uint ExceptionMaskBadAccess = 1u << 1;
private const int ExceptionDefault = 1;
private const int ThreadStateNone = 13;
[ThreadStatic]
private static bool _guestFaultsUseSignals;
private static int _machExceptionPortWarning;
[DllImport("libSystem.dylib", EntryPoint = "mach_thread_self")]
private static extern uint MachThreadSelf();
[DllImport("libSystem.dylib", EntryPoint = "thread_set_exception_ports")]
private static extern int ThreadSetExceptionPorts(uint thread, uint exceptionMask, uint newPort, int behavior, int newFlavor);
[DllImport("libSystem.dylib", EntryPoint = "mach_port_deallocate")]
private static extern int MachPortDeallocate(uint task, uint name);
private static readonly uint _machTaskSelf = OperatingSystem.IsMacOS()
? (uint)Marshal.ReadInt32(NativeLibrary.GetExport(NativeLibrary.Load("libSystem.dylib"), "mach_task_self_"))
: 0;
private static void RouteGuestAccessFaultsToSignals()
{
if (_guestFaultsUseSignals || !OperatingSystem.IsMacOS())
return;
_guestFaultsUseSignals = true;
var thread = MachThreadSelf();
var result = ThreadSetExceptionPorts(thread, ExceptionMaskBadAccess, 0, ExceptionDefault, ThreadStateNone);
_ = MachPortDeallocate(_machTaskSelf, thread);
if (result != 0 && System.Threading.Interlocked.Exchange(ref _machExceptionPortWarning, 1) == 0)
Console.Error.WriteLine($"[LOADER][WARN] Cannot hand guest access faults to the signal handler: thread_set_exception_ports={result}.");
}
}

View File

@ -79,7 +79,7 @@ public sealed partial class DirectExecutionBackend
// Callers set the Active* thread-statics before emitting the stub and read the
// yield/forced-exit flags right after this returns, so the worker outcome is
// copied back into this thread's statics before returning.
private unsafe ulong RunGuestEntryStub(void* entryStub, ulong hostRspSlot, bool requireNativeWorker = false)
private unsafe int RunGuestEntryStub(void* entryStub, ulong hostRspSlot, bool requireNativeWorker = false)
{
// Limit in-flight native Runs before renting so the idle pool is not
// drained by threads blocked on the concurrency gate.
@ -124,7 +124,7 @@ public sealed partial class DirectExecutionBackend
_activeGuestThreadYieldRequested = true;
_activeGuestThreadYieldReason = "tbb_native_worker_unavailable";
_activeForcedGuestExit = true;
return 0x80020012UL;
return unchecked((int)0x80020012);
}
TlsSetValue(_hostRspSlotTlsIndex, (nint)hostRspSlot);
@ -134,7 +134,7 @@ public sealed partial class DirectExecutionBackend
try
{
var state = _activeGuestThreadState;
if (LogThreadMode && state is { Name: "tbb_thead" })
if (state is { Name: "tbb_thead" })
{
var n = Interlocked.Increment(ref _tbbNativeRunEnterCount);
if (n <= 12 || n % 64 == 0)
@ -302,8 +302,8 @@ public sealed partial class DirectExecutionBackend
// loop: WaitForSingleObject(work);
// if (*stopFlag) { SetEvent(done); ExitThread(0); }
// rax = RunPrologue(self); // managed: binds per-run ambient, returns stub
// if (rax != 0) rax = rax(); // guest entry stub — zero managed frames below
// RunEpilogue(self, rax); // managed: captures outcome, restores ambient
// if (rax != 0) eax = rax(); // guest entry stub — zero managed frames below
// RunEpilogue(self, eax); // managed: captures outcome, restores ambient
// SetEvent(done); goto loop;
//
// Workers carry no per-guest identity of their own: the prologue rebinds guest TLS,
@ -348,7 +348,7 @@ public sealed partial class DirectExecutionBackend
private nint _runHostRspSlot;
private nint _runEntryStub;
private ulong _runAffinityMask;
private ulong _runNativeResult;
private int _runNativeResult;
private bool _runYieldRequested;
private string? _runYieldReason;
private bool _runForcedExit;
@ -425,7 +425,7 @@ public sealed partial class DirectExecutionBackend
}
var prologuePtr = (nint)(delegate* unmanaged<nint, nint>)&RunPrologue;
var epiloguePtr = (nint)(delegate* unmanaged<nint, ulong, void>)&RunEpilogue;
var epiloguePtr = (nint)(delegate* unmanaged<nint, int, void>)&RunEpilogue;
var executorHandle = GCHandle.ToIntPtr(_selfHandle);
nint workHandle;
nint doneHandle;
@ -504,9 +504,9 @@ public sealed partial class DirectExecutionBackend
Emit(0x0F); Emit(0x84); // je skipEntry
int skipJump = offset;
offset += sizeof(int);
EmitCallRax(); // guest entry stub -> rax
EmitCallRax(); // guest entry stub -> eax
int skipEntryOffset = offset;
Emit(0x48); Emit(0x89); Emit(0xC2); // mov rdx, rax
Emit(0x89); Emit(0xC2); // mov edx, eax
EmitMovRcxImm64((ulong)executorHandle);
EmitMovRaxImm64((ulong)epiloguePtr);
EmitCallRax();
@ -565,7 +565,7 @@ public sealed partial class DirectExecutionBackend
return true;
}
public ulong Run(
public int Run(
CpuContext context,
GuestThreadState? state,
ulong guestThreadHandle,
@ -657,7 +657,7 @@ public sealed partial class DirectExecutionBackend
yieldRequested = true;
yieldReason = "tbb_worker_prologue_fault";
forcedExit = true;
return 0x80020012UL;
return unchecked((int)0x80020012);
}
return _runNativeResult;
}
@ -707,7 +707,7 @@ public sealed partial class DirectExecutionBackend
}
[UnmanagedCallersOnly]
private static void RunEpilogue(nint executorHandle, ulong nativeResult)
private static void RunEpilogue(nint executorHandle, int nativeResult)
{
try
{
@ -750,7 +750,6 @@ public sealed partial class DirectExecutionBackend
_activeGuestThreadYieldReason = null;
_activeGuestThreadState = _runState;
backend.BindTlsBase(_runContext!);
RouteGuestAccessFaultsToSignals();
TlsSetValue(backend._hostRspSlotTlsIndex, _runHostRspSlot);
if (backend._workerDoneEventTlsIndex != uint.MaxValue)
{
@ -782,7 +781,7 @@ public sealed partial class DirectExecutionBackend
return _runEntryStub;
}
private void ExitRun(ulong nativeResult)
private void ExitRun(int nativeResult)
{
_runNativeResult = nativeResult;
_runYieldRequested = _activeGuestThreadYieldRequested;
@ -822,7 +821,7 @@ public sealed partial class DirectExecutionBackend
if (LogThreadMode)
{
TraceThreadMode(
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X16} yield={_runYieldRequested}");
$"worker_exit guest=0x{_runGuestThreadHandle:X16} result=0x{nativeResult:X8} yield={_runYieldRequested}");
}
}

View File

@ -1,140 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System;
using System.Runtime.InteropServices;
using Iced.Intel;
using SharpEmu.HLE;
using static Iced.Intel.AssemblerRegisters;
namespace SharpEmu.Core.Cpu.Native;
// Guest faults are delivered on the guest stack. The runtime only knows the thread's own stack,
// so managed code that runs on the guest stack can make a garbage collection misread the frames
// of that thread. Windows swaps the stack bounds in the TEB while guest code runs; POSIX cannot,
// so the signal handler moves to the host stack below the frames that entered guest code first.
public sealed unsafe partial class DirectExecutionBackend
{
// Host stack below the saved host RSP that is still treated as the host frames in use.
private const ulong HostStackSearchBytes = 0x100_0000;
private const int SignalStackGap = 0x1000;
// [0] the host RSP TLS slot (uint.MaxValue while unknown), [8] the Win64 TlsGetValue stub.
private static nint _signalStackCells;
private static unsafe nint CreatePosixSignalTrampoline()
{
_signalStackCells = (nint)NativeMemory.AllocZeroed(16);
*(uint*)_signalStackCells = uint.MaxValue;
var a = new Assembler(64);
var direct = a.CreateLabel();
var call = a.CreateLabel();
var done = a.CreateLabel();
a.push(rbx);
a.push(r12);
a.push(r13);
a.push(r14);
a.push(r15);
a.mov(r12d, edi);
a.mov(r13, rsi);
a.mov(r14, rdx);
a.mov(rbx, rsp);
// The saved host RSP of this thread, when it entered guest code.
a.mov(rax, (ulong)_signalStackCells);
a.mov(ecx, __dword_ptr[rax]);
a.cmp(ecx, -1);
a.je(direct);
a.mov(rax, __qword_ptr[rax + 8]);
a.test(rax, rax);
a.je(direct);
a.sub(rsp, 48);
a.call(rax);
a.add(rsp, 48);
a.test(rax, rax);
a.je(direct);
a.mov(r15, __qword_ptr[rax]);
a.test(r15, r15);
a.je(direct);
// Already on the host stack below the saved RSP: an import or host code faulted.
a.cmp(rbx, r15);
a.jae(call);
a.mov(rax, r15);
a.sub(rax, rbx);
a.cmp(rax, (int)HostStackSearchBytes);
a.jb(direct);
a.Label(ref call);
a.lea(rsp, __[r15 - SignalStackGap]);
a.and(rsp, -16);
a.Label(ref direct);
a.mov(edi, r12d);
a.mov(rsi, r13);
a.mov(rdx, r14);
a.mov(rax, (ulong)(nint)(delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal);
a.call(rax);
a.Label(ref done);
a.mov(rsp, rbx);
a.pop(r15);
a.pop(r14);
a.pop(r13);
a.pop(r12);
a.pop(rbx);
a.ret();
var page = (byte*)HostMemory.Alloc(null, 4096, HostMemory.MEM_COMMIT | HostMemory.MEM_RESERVE, HostMemory.PAGE_EXECUTE_READWRITE);
if (page == null)
{
throw new OutOfMemoryException("Failed to allocate the POSIX signal trampoline page");
}
var writer = new SpanCodeWriter(new Span<byte>(page, 4096));
a.Assemble(writer, (ulong)page);
if (!HostMemory.Protect(page, 4096, HostMemory.PAGE_EXECUTE_READ, out _))
{
throw new InvalidOperationException("Failed to protect the POSIX signal trampoline page");
}
return (nint)page;
}
private unsafe void PublishSignalStackState()
{
if (_signalStackCells == 0)
{
return;
}
*(nint*)(_signalStackCells + 8) = _tlsGetValueAddress;
*(uint*)_signalStackCells = _hostRspSlotTlsIndex;
}
private sealed unsafe class SpanCodeWriter : CodeWriter
{
private readonly byte* _start;
private readonly int _length;
private int _offset;
public SpanCodeWriter(Span<byte> destination)
{
fixed (byte* start = destination)
{
_start = start;
}
_length = destination.Length;
}
public override void WriteByte(byte value)
{
if (_offset >= _length)
{
throw new InvalidOperationException("The POSIX signal trampoline does not fit its page.");
}
_start[_offset++] = value;
}
}
}

View File

@ -5,7 +5,6 @@ using System;
using System.Runtime.InteropServices;
using System.Threading;
using SharpEmu.HLE;
using SharpEmu.HLE.GpuMemory;
namespace SharpEmu.Core.Cpu.Native;
@ -24,7 +23,6 @@ public sealed unsafe partial class DirectExecutionBackend
private const int PosixSigIll = 4;
private const int PosixSigTrap = 5;
private const int PosixSigAbort = 6;
private const int PosixSigFpe = 8;
private const int PosixSigSegv = 11;
private static readonly int PosixSigBus = OperatingSystem.IsMacOS() ? 10 : 7;
@ -47,10 +45,6 @@ public sealed unsafe partial class DirectExecutionBackend
// registers in gregs[23]. Rosetta 2 delivers the regular x86-64 layout
// to translated processes.
private const int DarwinUcontextMcontextOffset = 48;
private const int DarwinUserContextMachineContextSizeOffset = 40;
// Darwin stores vector registers at the same offset in both signal frame formats.
// The system headers i386/_mcontext.h and mach/i386/_structs.h define this offset.
private const int DarwinMachineContextVectorRegistersOffset = 352;
private const int DarwinMcontextErrOffset = 4;
private const int DarwinMcontextFaultAddressOffset = 8;
private const int LinuxUcontextGregsOffset = 40;
@ -79,7 +73,6 @@ public sealed unsafe partial class DirectExecutionBackend
private static DirectExecutionBackend? _posixSignalBackend;
private static bool _posixSignalHandlersInstalled;
private static nint _posixSignalTrampoline;
private static bool _posixRawRecoveryEnabled;
private static bool _posixSignalWarmup;
private static readonly nint[] _posixPreviousActions = new nint[32];
@ -91,8 +84,12 @@ public sealed unsafe partial class DirectExecutionBackend
[ThreadStatic]
private static int _posixSignalHandlerDepth;
// True when the signal context contains the current vector register values.
// Copy changes back to these registers before the guest continues.
// True while the current thread's in-flight POSIX fault carries the real
// XMM registers in the CONTEXT scratch buffer and writes to them will
// reach the mcontext on resume. Gates recovery paths (SSE4a EXTRQ/
// INSERTQ) that would otherwise compute results from a zeroed XMM area
// and silently discard what they "wrote". Darwin is not bridged yet, so
// the flag stays false there.
[ThreadStatic]
private static bool _posixXmmContextBridged;
@ -107,7 +104,6 @@ public sealed unsafe partial class DirectExecutionBackend
_posixSignalBackend = this;
if (_posixSignalHandlersInstalled)
{
PublishSignalStackState();
return;
}
@ -121,20 +117,17 @@ public sealed unsafe partial class DirectExecutionBackend
WarmUpPosixSignalPath();
SharpEmu.HLE.GuestImageWriteTracker.WarmUp();
_posixSignalTrampoline = CreatePosixSignalTrampoline();
PublishSignalStackState();
if (!InstallPosixSignalHandler(PosixSigSegv) ||
!InstallPosixSignalHandler(PosixSigBus) ||
!InstallPosixSignalHandler(PosixSigIll) ||
!InstallPosixSignalHandler(PosixSigTrap) ||
!InstallPosixSignalHandler(PosixSigAbort) ||
!InstallPosixSignalHandler(PosixSigFpe))
!InstallPosixSignalHandler(PosixSigAbort))
{
throw new InvalidOperationException("Failed to install POSIX fault signal handlers");
}
_posixSignalHandlersInstalled = true;
Console.Error.WriteLine("[LOADER][INFO] POSIX signal exception bridge installed (SIGSEGV/SIGBUS/SIGILL/SIGFPE)");
Console.Error.WriteLine("[LOADER][INFO] POSIX signal exception bridge installed (SIGSEGV/SIGBUS/SIGILL)");
}
/// <summary>
@ -148,23 +141,19 @@ public sealed unsafe partial class DirectExecutionBackend
/// </summary>
private void WarmUpPosixSignalPath()
{
byte* testUserContext = stackalloc byte[512];
new Span<byte>(testUserContext, 512).Clear();
byte* testMachineContext = stackalloc byte[1024];
new Span<byte>(testMachineContext, 1024).Clear();
byte* fakeUcontext = stackalloc byte[512];
new Span<byte>(fakeUcontext, 512).Clear();
byte* fakeMcontext = stackalloc byte[512];
new Span<byte>(fakeMcontext, 512).Clear();
if (OperatingSystem.IsMacOS())
{
*(byte**)(testUserContext + DarwinUcontextMcontextOffset) = testMachineContext;
*(ulong*)(testUserContext + DarwinUserContextMachineContextSizeOffset) = 1024;
// Load the Mach memory functions before the first signal uses them.
byte readProbe = 0;
_ = TryReadMacOsMemory((ulong)testMachineContext, &readProbe, 1);
*(byte**)(fakeUcontext + DarwinUcontextMcontextOffset) = fakeMcontext;
}
_posixSignalWarmup = true;
try
{
((delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal)(PosixSigSegv, 0, (nint)testUserContext);
((delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal)(PosixSigSegv, 0, (nint)fakeUcontext);
// Warm the branches the fabricated fault above skips without
// spamming diagnostics: the benign-exception path through
@ -198,7 +187,7 @@ public sealed unsafe partial class DirectExecutionBackend
{
byte* action = stackalloc byte[PosixSigactionSize];
new Span<byte>(action, PosixSigactionSize).Clear();
*(nint*)action = _posixSignalTrampoline;
*(nint*)action = (nint)(delegate* unmanaged<int, nint, nint, void>)&HandlePosixSignal;
// No SA_ONSTACK: the runtime's alternate stacks are far too small for
// the recovery/diagnostic path (JIT compilation of cold handler code
// can run inside the signal frame). Guest faults deliver onto the 2MB
@ -245,7 +234,6 @@ public sealed unsafe partial class DirectExecutionBackend
// address (safe for host and guest threads alike) and must resume
// the faulting write immediately after restoring write access.
if (signal != PosixSigIll &&
signal != PosixSigFpe &&
siginfo != 0 &&
SharpEmu.HLE.GuestImageWriteTracker.TryHandleWriteFault(
*(ulong*)((byte*)siginfo + PosixSigInfoAddressOffset)))
@ -253,17 +241,6 @@ public sealed unsafe partial class DirectExecutionBackend
return;
}
// GPU-tracked pages fault on every first CPU access after the GPU or an upload used them,
// thousands of times a second in a frame. Resolving them needs only the address and the
// access kind, so they skip the Win64 context conversion and the other handlers.
if (PosixGpuFaultFastPath && !_posixSignalWarmup &&
(signal == PosixSigSegv || signal == PosixSigBus) &&
siginfo != 0 &&
TryResolvePosixGpuFault(siginfo, ucontext))
{
return;
}
if (TryHandlePosixFault(signal, siginfo, ucontext))
{
return;
@ -281,28 +258,6 @@ public sealed unsafe partial class DirectExecutionBackend
ChainPreviousPosixAction(signal, siginfo, ucontext);
}
private static readonly bool PosixGpuFaultFastPath =
Environment.GetEnvironmentVariable("SHARPEMU_POSIX_GPU_FAULT_FAST_PATH") != "0";
private static bool TryResolvePosixGpuFault(nint siginfo, nint ucontext)
{
byte* registers = GetPosixRegisterBase(ucontext);
if (registers == null)
{
return false;
}
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
ulong rip = *(ulong*)(registers + PosixRegisterOffsets[16]);
var kind = GetPosixAccessType(registers, faultAddress, rip) switch
{
0 => FaultKind.Read,
1 => FaultKind.Write,
_ => FaultKind.Unknown,
};
return kind != FaultKind.Unknown && GuestGpuMemoryHook.TryResolveFault(kind, faultAddress);
}
private static bool TryHandlePosixFault(int signal, nint siginfo, nint ucontext)
{
byte* registers = GetPosixRegisterBase(ucontext);
@ -319,18 +274,25 @@ public sealed unsafe partial class DirectExecutionBackend
WriteCtxU64(contextRecord, CTX_RAX + i * 8, *(ulong*)(registers + offsets[i]));
}
// Copy the vector registers that instruction recovery can change.
// Keep all other floating-point state unchanged.
byte* vectorRegisters = GetSignalVectorRegisterAddress(ucontext, registers);
if (vectorRegisters != null)
// Bridge the XMM registers alongside the GPRs where the layout is
// known: on Linux the fpstate pointer and FXSAVE image are kernel
// ABI, so recovery paths that read or write XMM state (SSE4a
// EXTRQ/INSERTQ) see the live registers and their writes reach the
// guest through sigreturn.
byte* fpstate = null;
if (OperatingSystem.IsLinux())
{
Buffer.MemoryCopy(
vectorRegisters,
contextRecord + Win64ContextXmm0Offset,
XmmBlockSize,
XmmBlockSize);
fpstate = *(byte**)(registers + LinuxGregsFpstateOffset);
if (fpstate != null)
{
Buffer.MemoryCopy(
fpstate + FxsaveXmmOffset,
contextRecord + Win64ContextXmm0Offset,
XmmBlockSize,
XmmBlockSize);
}
}
_posixXmmContextBridged = vectorRegisters != null;
_posixXmmContextBridged = fpstate != null;
EXCEPTION_RECORD record = default;
record.ExceptionAddress = (void*)ReadCtxU64(contextRecord, CTX_RIP);
@ -346,11 +308,6 @@ public sealed unsafe partial class DirectExecutionBackend
{
record.ExceptionCode = 1073741845u;
}
else if (signal == PosixSigFpe)
{
// STATUS_INTEGER_DIVIDE_BY_ZERO: #DE from div/idiv (zero divisor or quotient overflow).
record.ExceptionCode = 3221225620u;
}
else
{
ulong faultAddress = GetPosixFaultAddress(siginfo, registers);
@ -402,36 +359,17 @@ public sealed unsafe partial class DirectExecutionBackend
{
*(ulong*)(registers + offsets[i]) = ReadCtxU64(contextRecord, CTX_RAX + i * 8);
}
if (vectorRegisters != null)
if (fpstate != null)
{
Buffer.MemoryCopy(
contextRecord + Win64ContextXmm0Offset,
vectorRegisters,
fpstate + FxsaveXmmOffset,
XmmBlockSize,
XmmBlockSize);
}
return true;
}
private static byte* GetSignalVectorRegisterAddress(nint userContextAddress, byte* machineContext)
{
if (OperatingSystem.IsMacOS())
{
var machineContextSize = *(ulong*)((byte*)userContextAddress + DarwinUserContextMachineContextSizeOffset);
return machineContextSize >= DarwinMachineContextVectorRegistersOffset + XmmBlockSize
? machineContext + DarwinMachineContextVectorRegistersOffset
: null;
}
if (OperatingSystem.IsLinux())
{
byte* floatingPointState = *(byte**)(machineContext + LinuxGregsFpstateOffset);
return floatingPointState != null ? floatingPointState + FxsaveXmmOffset : null;
}
return null;
}
private static byte* GetPosixRegisterBase(nint ucontext)
{
if (ucontext == 0)

View File

@ -1,107 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Buffers.Binary;
using System.Threading;
using Iced.Intel;
using SharpEmu.Core.Cpu.Emulation;
namespace SharpEmu.Core.Cpu.Native;
// The PS5 CPU has the SHA extensions and titles use them without a CPUID check.
// Rosetta 2 does not translate them, so they raise #UD on Apple Silicon hosts.
public sealed partial class DirectExecutionBackend
{
private static int _shaSoftwareFallbackAnnounced;
private static long _shaInstructionsEmulated;
private unsafe bool TryRecoverShaInstruction(void* contextRecord, ulong rip)
{
if (!OperatingSystem.IsWindows() && !_posixXmmContextBridged ||
!TryReadFaultingInstruction(rip, out var instruction))
{
return false;
}
switch (instruction.Mnemonic)
{
case Mnemonic.Sha1rnds4:
case Mnemonic.Sha1nexte:
case Mnemonic.Sha1msg1:
case Mnemonic.Sha1msg2:
case Mnemonic.Sha256rnds2:
case Mnemonic.Sha256msg1:
case Mnemonic.Sha256msg2:
break;
default:
return false;
}
if (instruction.GetOpKind(0) != OpKind.Register ||
!TryGetXmmOffset(instruction.GetOpRegister(0), out var destOffset) ||
!TryReadXmmOperand(contextRecord, in instruction, 1, out var source2))
{
return false;
}
var source1 = ReadCtxXmm(contextRecord, destOffset);
var result = instruction.Mnemonic switch
{
Mnemonic.Sha1rnds4 => ShaInstructionEmulator.Sha1Rnds4(source1, source2, instruction.Immediate8),
Mnemonic.Sha1nexte => ShaInstructionEmulator.Sha1Nexte(source1, source2),
Mnemonic.Sha1msg1 => ShaInstructionEmulator.Sha1Msg1(source1, source2),
Mnemonic.Sha1msg2 => ShaInstructionEmulator.Sha1Msg2(source1, source2),
Mnemonic.Sha256rnds2 => ShaInstructionEmulator.Sha256Rnds2(
source1, source2, ReadCtxXmm(contextRecord, Win64ContextXmm0Offset)),
Mnemonic.Sha256msg1 => ShaInstructionEmulator.Sha256Msg1(source1, source2),
_ => ShaInstructionEmulator.Sha256Msg2(source1, source2),
};
WriteCtxU64(contextRecord, destOffset, (ulong)result);
WriteCtxU64(contextRecord, destOffset + 8, (ulong)(result >> 64));
WriteCtxU64(contextRecord, CTX_RIP, rip + (ulong)instruction.Length);
Interlocked.Increment(ref _shaInstructionsEmulated);
if (Interlocked.Exchange(ref _shaSoftwareFallbackAnnounced, 1) == 0)
{
Console.Error.WriteLine(
"[LOADER][INFO] Host lacks the SHA extensions used by the guest; " +
"emulating those instructions in software.");
}
return true;
}
private unsafe bool TryReadXmmOperand(void* contextRecord, in Instruction instruction, int operandIndex, out UInt128 value)
{
value = 0;
switch (instruction.GetOpKind(operandIndex))
{
case OpKind.Register:
if (!TryGetXmmOffset(instruction.GetOpRegister(operandIndex), out var offset))
{
return false;
}
value = ReadCtxXmm(contextRecord, offset);
return true;
case OpKind.Memory:
var buffer = new byte[16];
if (!TryComputeMemoryAddress(contextRecord, in instruction, out var address) ||
!TryReadHostBytes(address, buffer))
{
return false;
}
value = BinaryPrimitives.ReadUInt128LittleEndian(buffer);
return true;
default:
return false;
}
}
private static unsafe UInt128 ReadCtxXmm(void* contextRecord, int offset) =>
new(ReadCtxU64(contextRecord, offset + 8), ReadCtxU64(contextRecord, offset));
}

View File

@ -1,206 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
namespace SharpEmu.Core.Cpu.Native;
public sealed partial class DirectExecutionBackend
{
private const int HostStackStateBytes = 3 * sizeof(ulong);
internal static unsafe bool TryGetGuestStackBounds(ulong stackPointer, out ulong bottom, out ulong top)
{
if (OperatingSystem.IsWindows() &&
QueryVirtualMemoryInformation(-1, (void*)stackPointer, 0, out var allocation,
(nuint)sizeof(StackMemoryRegionInformation), null) != 0 &&
((allocation.Flags & 1) == 0 || allocation.CommitSize == allocation.RegionSize) &&
allocation.RegionSize != 0 && allocation.RegionSize <= ulong.MaxValue - allocation.AllocationBase &&
stackPointer >= allocation.AllocationBase && stackPointer - allocation.AllocationBase < allocation.RegionSize)
{
bottom = allocation.AllocationBase;
top = bottom + allocation.RegionSize;
return true;
}
bottom = top = 0;
var queried = VirtualQuery((void*)stackPointer, out var region, (nuint)sizeof(MEMORY_BASIC_INFORMATION64));
if (queried == 0 ||
region.State != 0x1000 || region.RegionSize == 0 || region.RegionSize > ulong.MaxValue - region.BaseAddress)
{
return false;
}
bottom = region.BaseAddress;
top = region.BaseAddress + region.RegionSize;
return stackPointer >= bottom && stackPointer < top;
}
private static unsafe void EmitStackBound(byte* code, ref int offset, byte register, uint field, bool store)
{
EmitByte(code, ref offset, 0x65);
EmitByte(code, ref offset, (byte)(0x48 | (register >= 8 ? 4 : 0)));
EmitByte(code, ref offset, store ? (byte)0x89 : (byte)0x8B);
EmitByte(code, ref offset, (byte)(0x04 | ((register & 7) << 3)));
EmitByte(code, ref offset, 0x25);
EmitUInt32(code, ref offset, field);
}
// Fiber switches replace the guest stack without passing through a guest-entry stub, so the
// context-transfer stub keeps the Windows TEB bounds in sync from the target state in R11
// (stack top at +160, bottom at +168). Only Windows keeps stack bounds at gs:[8]/gs:[16]: on
// macOS gs addresses the pthread TSD, where those slots hold errno's address and another key.
// Writing a fiber's bounds there nulled errno, and the next failed libc call on the thread
// crashed reading it (Demon's Souls, about 10 s into boot).
private static unsafe void EmitFiberStackBounds(byte* code, ref int offset)
{
if (!OperatingSystem.IsWindows()) return;
ReadOnlySpan<byte> loadTop = [0x4D, 0x8B, 0x93, 160, 0, 0, 0]; // mov r10, [r11+160]
foreach (var value in loadTop) EmitByte(code, ref offset, value);
EmitStackBound(code, ref offset, 10, 8, store: true);
ReadOnlySpan<byte> loadBottom = [0x4D, 0x8B, 0x93, 168, 0, 0, 0]; // mov r10, [r11+168]
foreach (var value in loadBottom) EmitByte(code, ref offset, value);
EmitStackBound(code, ref offset, 10, 16, store: true);
}
// Test hook: the bytes the fiber stack-bounds sync emits on this host.
internal static unsafe byte[] EmitFiberStackBoundsForTest()
{
var buffer = new byte[64];
var offset = 0;
fixed (byte* code = buffer)
{
EmitFiberStackBounds(code, ref offset);
}
return buffer[..offset];
}
// R10 points to the entry state. R11 is scratch; RAX remains unchanged.
private static unsafe void EmitHostStackBounds(byte* code, ref int offset, bool save)
{
if (!OperatingSystem.IsWindows()) return;
for (byte field = 8; field <= 16; field += 8)
{
if (save) EmitStackBound(code, ref offset, 11, field, store: false);
EmitByte(code, ref offset, 0x4D);
EmitByte(code, ref offset, save ? (byte)0x89 : (byte)0x8B);
EmitByte(code, ref offset, 0x5A);
EmitByte(code, ref offset, field);
if (!save) EmitStackBound(code, ref offset, 11, field, store: true);
}
}
private static unsafe void EmitGuestStackBounds(byte* code, ref int offset, ulong stackPointer)
{
if (!OperatingSystem.IsWindows()) return;
if (!TryGetGuestStackBounds(stackPointer, out var bottom, out var top))
{
throw new InvalidOperationException("Guest stack has no committed host mapping.");
}
EmitHostStackBounds(code, ref offset, save: true);
for (uint field = 8; field <= 16; field += 8)
{
EmitByte(code, ref offset, 0x49);
EmitByte(code, ref offset, 0xBB);
*(ulong*)(code + offset) = field == 8 ? top : bottom;
offset += sizeof(ulong);
EmitStackBound(code, ref offset, 11, field, store: true);
}
}
private static unsafe void EmitSavedStackBounds(byte* code, ref int offset, bool restore)
{
if (!OperatingSystem.IsWindows()) return;
EmitStackBound(code, ref offset, 14, 8, restore);
EmitStackBound(code, ref offset, 15, 16, restore);
}
private unsafe void EmitExceptionHostStackBounds(byte* code, ref int offset)
{
EmitSavedStackBounds(code, ref offset, restore: false);
ReadOnlySpan<byte> reserve = [0x48, 0x83, 0xEC, 0x28, 0xB9];
foreach (var value in reserve) EmitByte(code, ref offset, value);
EmitUInt32(code, ref offset, _hostRspSlotTlsIndex);
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0xB8);
*(nint*)(code + offset) = _tlsGetValueAddress;
offset += sizeof(nint);
ReadOnlySpan<byte> lookup = [0xFF, 0xD0, 0x48, 0x83, 0xC4, 0x28, 0x48, 0x85, 0xC0, 0x0F, 0x84];
foreach (var value in lookup) EmitByte(code, ref offset, value);
var missingState = offset;
EmitUInt32(code, ref offset, 0);
// The state is zeroed at allocation; bounds the entry stub has not saved yet stay untouched.
ReadOnlySpan<byte> unsaved = [0x48, 0x83, 0x78, 0x08, 0x00, 0x0F, 0x84];
foreach (var value in unsaved) EmitByte(code, ref offset, value);
var missingBounds = offset;
EmitUInt32(code, ref offset, 0);
EmitByte(code, ref offset, 0x49); EmitByte(code, ref offset, 0x89); EmitByte(code, ref offset, 0xC2);
// A host fault already has live bounds. Do not replace a grown limit with an old one.
ReadOnlySpan<byte> sameStack = [0x4C, 0x3B, 0x70, 0x08, 0x0F, 0x85];
foreach (var value in sameStack) EmitByte(code, ref offset, value);
var guestStack = offset;
EmitUInt32(code, ref offset, 0);
EmitHostStackBounds(code, ref offset, save: true);
EmitByte(code, ref offset, 0xE9);
var hostStack = offset;
EmitUInt32(code, ref offset, 0);
*(int*)(code + guestStack) = offset - guestStack - sizeof(int);
EmitHostStackBounds(code, ref offset, save: false);
*(int*)(code + hostStack) = offset - hostStack - sizeof(int);
*(int*)(code + missingState) = offset - missingState - sizeof(int);
*(int*)(code + missingBounds) = offset - missingBounds - sizeof(int);
}
[UnmanagedCallersOnly]
private static unsafe int SelectExceptionStackBounds(void* exceptionInfo, int disposition, ulong* bounds)
{
if (disposition != -1) return disposition;
var context = ((EXCEPTION_POINTERS*)exceptionInfo)->ContextRecord;
if (context == null) return 0;
var stackPointer = ReadCtxU64(context, 152);
if (stackPointer >= bounds[1] && stackPointer < bounds[0]) return disposition;
GetCurrentThreadStackLimits(out var nativeBottom, out var nativeTop);
if (stackPointer >= nativeBottom && stackPointer < nativeTop)
{
bounds[0] = nativeTop;
bounds[1] = nativeBottom;
return disposition;
}
if (!TryGetGuestStackBounds(stackPointer, out var bottom, out var top)) return 0;
bounds[0] = top;
bounds[1] = bottom;
return disposition;
}
private static unsafe void EmitExceptionStackSelection(byte* code, ref int offset)
{
// The caller reserves 72 bytes: ABI home space and two stack bounds.
ReadOnlySpan<byte> arguments = [0x4C, 0x89, 0x74, 0x24, 0x28, 0x4C, 0x89, 0x7C, 0x24, 0x30,
0x89, 0xC2, 0x4C, 0x89, 0xE9, 0x4C, 0x8D, 0x44, 0x24, 0x28, 0x48, 0xB8];
foreach (var value in arguments) EmitByte(code, ref offset, value);
*(nint*)(code + offset) = (nint)(delegate* unmanaged<void*, int, ulong*, int>)&SelectExceptionStackBounds;
offset += sizeof(nint);
ReadOnlySpan<byte> result = [0xFF, 0xD0, 0x4C, 0x8B, 0x74, 0x24, 0x28, 0x4C, 0x8B, 0x7C, 0x24, 0x30];
foreach (var value in result) EmitByte(code, ref offset, value);
}
[DllImport("kernel32.dll", ExactSpelling = true)]
private static extern void GetCurrentThreadStackLimits(out ulong lowLimit, out ulong highLimit);
[StructLayout(LayoutKind.Sequential)]
private struct StackMemoryRegionInformation
{
public ulong AllocationBase;
public uint AllocationProtect;
public uint Flags;
public ulong RegionSize;
public ulong CommitSize;
}
[DllImport("api-ms-win-core-memory-l1-1-4.dll", ExactSpelling = true)]
private static extern unsafe int QueryVirtualMemoryInformation(nint process, void* address, int informationClass,
out StackMemoryRegionInformation information, nuint informationSize, nuint* returnSize);
}

View File

@ -1,182 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
using System.ComponentModel;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Core.Cpu.Native;
public sealed unsafe partial class DirectExecutionBackend
{
private SafeWaitHandle? _mainExecutionThreadHandle;
// One immutable code page serves all diagnostic samples for the process lifetime.
private static readonly Lazy<ThreadContextCaptureCode> ThreadCaptureCode = new(() => new());
private sealed class ThreadContextCaptureCode : SafeHandle
{
public override bool IsInvalid => handle == 0;
public ThreadContextCaptureCode() : base(0, ownsHandle: true)
{
var kernelModule = GetModuleHandle("kernel32.dll");
var suspendAddress = GetProcAddress(kernelModule, "SuspendThread");
var contextAddress = GetProcAddress(kernelModule, "GetThreadContext");
var resumeAddress = GetProcAddress(kernelModule, "ResumeThread");
if (suspendAddress == 0 || contextAddress == 0 || resumeAddress == 0) return;
var instructions = BuildThreadContextCaptureCode(suspendAddress, contextAddress, resumeAddress);
SetHandle((nint)VirtualAlloc(null, (nuint)instructions.Length, 0x3000, 0x04));
if (IsInvalid) return;
instructions.CopyTo(new Span<byte>((void*)handle, instructions.Length));
uint previousProtection = 0;
if (!VirtualProtect((void*)handle, (nuint)instructions.Length, 0x20, &previousProtection))
{
Dispose();
return;
}
FlushInstructionCache(GetCurrentProcess(), (void*)handle, (nuint)instructions.Length);
}
protected override bool ReleaseHandle() => VirtualFree((void*)handle, 0, 0x8000);
}
private static SafeWaitHandle? OpenThreadForContext(uint threadId)
{
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64 || threadId == 0)
return null;
const uint queryLimitedInformation = 0x0800;
var threadHandle = OpenThread(ThreadGetContext | ThreadSuspendResume | queryLimitedInformation, false, threadId);
return threadHandle == 0 ? null : new SafeWaitHandle(threadHandle, ownsHandle: true);
}
private static bool TryCaptureHostThreadContext(int threadId, out HostThreadContextSnapshot snapshot)
{
using var threadHandle = OpenThreadForContext(unchecked((uint)threadId));
return TryCaptureThreadContext(threadHandle, out snapshot);
}
private static bool TryCaptureThreadContext(SafeWaitHandle? threadHandle, out HostThreadContextSnapshot snapshot)
{
snapshot = default;
if (threadHandle is null || !OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
return false;
var handleReferenced = false;
try
{
threadHandle.DangerousAddRef(ref handleReferenced);
var nativeHandle = threadHandle.DangerousGetHandle();
var threadId = GetThreadId(nativeHandle);
if (threadId == 0 || threadId == GetCurrentThreadId()) return false;
var captureCode = ThreadCaptureCode.Value;
if (captureCode.IsInvalid || captureCode.IsClosed) return false;
byte* contextStorage = stackalloc byte[Win64ContextSize + 15];
var contextRecord = (void*)(((nuint)contextStorage + 15) & ~(nuint)15);
new Span<byte>(contextRecord, Win64ContextSize).Clear();
WriteCtxU32(contextRecord, Win64ContextFlagsOffset, ContextAmd64ControlInteger);
// One native transition keeps managed work outside the suspended interval.
var captured = ((delegate* unmanaged<nint, void*, int>)captureCode.DangerousGetHandle())(nativeHandle, contextRecord);
if (captured == 0) return false;
snapshot = DecodeHostThreadContext(threadId, contextRecord);
return true;
}
catch (ObjectDisposedException)
{
return false;
}
finally
{
if (handleReferenced) threadHandle!.DangerousRelease();
}
}
private static HostThreadContextSnapshot DecodeHostThreadContext(uint threadId, void* contextRecord) => new(
threadId,
ReadCtxU64(contextRecord, 248), ReadCtxU64(contextRecord, 152), ReadCtxU64(contextRecord, 160),
ReadCtxU64(contextRecord, 120), ReadCtxU64(contextRecord, 144), ReadCtxU64(contextRecord, 128),
ReadCtxU64(contextRecord, 136), ReadCtxU64(contextRecord, 168), ReadCtxU64(contextRecord, 176),
ReadCtxU64(contextRecord, 184), ReadCtxU64(contextRecord, 192), ReadCtxU64(contextRecord, 200),
ReadCtxU64(contextRecord, 208), ReadCtxU64(contextRecord, 216), ReadCtxU64(contextRecord, 224),
ReadCtxU64(contextRecord, 232), ReadCtxU64(contextRecord, 240));
internal static byte[] BuildThreadContextCaptureCode(nint suspendAddress, nint contextAddress, nint resumeAddress)
{
var instructions = new List<byte>();
void CallAddress(nint address)
{
instructions.AddRange([0x48, 0xB8]);
instructions.AddRange(BitConverter.GetBytes((long)address));
instructions.AddRange([0xFF, 0xD0]);
}
// Preserve nonvolatile registers and reserve aligned argument space.
instructions.AddRange([0x53, 0x56, 0x57, 0x48, 0x83, 0xEC, 0x20,
0x48, 0x89, 0xCB, 0x48, 0x89, 0xD6, 0x31, 0xFF]);
CallAddress(suspendAddress);
instructions.AddRange([0x83, 0xF8, 0xFF, 0x74, 0]);
var failedSuspendJump = instructions.Count - 1;
instructions.AddRange([0x48, 0x89, 0xD9, 0x48, 0x89, 0xF2]);
CallAddress(contextAddress);
instructions.AddRange([0x89, 0xC7, 0x48, 0x89, 0xD9]);
CallAddress(resumeAddress);
instructions.AddRange([0x83, 0xF8, 0xFF, 0x75, 0x02, 0x31, 0xFF]);
var returnOffset = instructions.Count;
instructions.AddRange([0x89, 0xF8, 0x48, 0x83, 0xC4, 0x20, 0x5F, 0x5E, 0x5B, 0xC3]);
instructions[failedSuspendJump] = checked((byte)(returnOffset - failedSuspendJump - 1));
return instructions.ToArray();
}
private void LogMainThreadStallContext()
{
if (!TryCaptureThreadContext(Volatile.Read(ref _mainExecutionThreadHandle), out var context)) return;
Console.Error.WriteLine($"[LOADER][ERROR] Live hardware context (main thread {context.ThreadId}):");
Console.Error.WriteLine($"[LOADER][ERROR] RIP=0x{context.Rip:X16} RSP=0x{context.Rsp:X16} RBP=0x{context.Rbp:X16}");
Console.Error.WriteLine($"[LOADER][ERROR] RAX=0x{context.Rax:X16} RBX=0x{context.Rbx:X16} RCX=0x{context.Rcx:X16} RDX=0x{context.Rdx:X16}");
Console.Error.WriteLine($"[LOADER][ERROR] RSI=0x{context.Rsi:X16} RDI=0x{context.Rdi:X16} R8=0x{context.R8:X16} R9=0x{context.R9:X16}");
Console.Error.WriteLine($"[LOADER][ERROR] R10=0x{context.R10:X16} R11=0x{context.R11:X16} R12=0x{context.R12:X16} R13=0x{context.R13:X16} R14=0x{context.R14:X16} R15=0x{context.R15:X16}");
var modules = Array.Empty<(ulong Start, ulong Size, string Name)>();
try
{
using var process = Process.GetCurrentProcess();
modules = process.Modules.Cast<ProcessModule>().Select(module =>
(Start: unchecked((ulong)module.BaseAddress), Size: (ulong)module.ModuleMemorySize, Name: module.ModuleName)).ToArray();
}
catch (Win32Exception) { }
catch (InvalidOperationException) { }
string DescribeAddress(ulong address)
{
foreach (var module in modules)
if (address >= module.Start && address - module.Start < module.Size)
return $"{module.Name}+0x{address - module.Start:X}";
return $"0x{address:X16}";
}
Console.Error.WriteLine($"[LOADER][ERROR] RIP location: {DescribeAddress(context.Rip)}");
// The thread has resumed. These bytes are best-effort observations, not an atomic snapshot.
var instructionBytes = new byte[48];
if (context.Rip >= 32 && TryReadHostBytes(context.Rip - 32, instructionBytes))
Console.Error.WriteLine($"[LOADER][ERROR] Live bytes @[RIP-32..RIP+16]: {Convert.ToHexString(instructionBytes)}");
var currentInstruction = new byte[16];
if (TryReadHostBytes(context.Rip, currentInstruction))
Console.Error.WriteLine($"[LOADER][ERROR] Live bytes @RIP: {Convert.ToHexString(currentInstruction)}");
Console.Error.WriteLine("[LOADER][ERROR] Live stack pointer candidates:");
for (var offset = 0; offset < 256; offset += sizeof(ulong))
{
var address = context.Rsp + (ulong)offset;
if (address < context.Rsp || !TryReadDiagnosticHostQword(address, out var value)) break;
if (value != 0)
Console.Error.WriteLine($"[LOADER][ERROR] [rsp+0x{offset:X2}] = 0x{value:X16} ({DescribeAddress(value)})");
}
}
[DllImport("kernel32.dll")]
private static extern uint GetThreadId(nint threadHandle);
}

File diff suppressed because it is too large Load Diff

View File

@ -1,100 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
namespace SharpEmu.Core.Cpu.Native;
internal sealed class GuestThreadFlowProfile(int capacity = 131072)
{
internal static readonly bool Enabled =
(Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE") == "1" ||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_GUEST_SCHEDULER") == "1") &&
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE") == "1";
internal enum EventKind
{
Blocked, Ready, Claimed, Scheduled, RunStarted, RunStopped, CallbackResumed,
ExecutorReleased, ClaimDeferred
}
internal readonly record struct TraceEvent(long Timestamp, int HostThreadId, ulong GuestThreadHandle,
string Name, EventKind Kind, string? Reason, string? WakeKey, string? ImportNid,
ulong Argument0, ulong Argument1, long DeadlineTimestamp);
internal readonly record struct Snapshot(TraceEvent[] Events, long TotalEvents);
private readonly int _capacity = capacity > 0 ? capacity : throw new ArgumentOutOfRangeException(nameof(capacity));
private readonly object _gate = new();
private readonly object _writeGate = new();
private TraceEvent[]? _events;
private long _eventCount;
private bool _closed;
internal void StartSession()
{
lock (_writeGate)
{
lock (_gate)
{
_events = null;
_eventCount = 0;
_closed = false;
}
}
}
internal void Record(in TraceEvent traceEvent)
{
lock (_gate)
{
if (_closed) return;
_events ??= new TraceEvent[_capacity];
_events[(int)(_eventCount % _capacity)] = traceEvent;
_eventCount++;
}
}
internal Snapshot Close()
{
lock (_gate)
{
_closed = true;
var retained = (int)Math.Min(_eventCount, _capacity);
var records = new TraceEvent[retained];
var first = _eventCount - retained;
for (var index = 0; index < retained; index++)
records[index] = _events![(int)((first + index) % _capacity)];
var snapshot = new Snapshot(records, _eventCount);
_events = null;
_eventCount = 0;
return snapshot;
}
}
internal void WriteTrace(TextWriter output)
{
// A concurrent shutdown caller must wait for the first writer to finish.
lock (_writeGate)
{
WriteRetainedEvents(output);
}
}
private void WriteRetainedEvents(TextWriter output)
{
var snapshot = Close();
if (snapshot.TotalEvents == 0) return;
output.WriteLine($"[PERF][GUEST_FLOW_TRACE] frequency={Stopwatch.Frequency} retained={snapshot.Events.Length} overwritten={snapshot.TotalEvents - snapshot.Events.Length}");
for (var index = 0; index < snapshot.Events.Length; index++)
{
var item = snapshot.Events[index];
output.WriteLine($"[PERF][GUEST_FLOW] sequence={snapshot.TotalEvents - snapshot.Events.Length + index} timestamp={item.Timestamp} thread={item.HostThreadId} guest=0x{item.GuestThreadHandle:X} name='{Quote(item.Name)}' event={item.Kind} reason='{Quote(item.Reason)}' wake='{Quote(item.WakeKey)}' nid='{Quote(item.ImportNid)}' arg0=0x{item.Argument0:X} arg1=0x{item.Argument1:X} deadline={item.DeadlineTimestamp}");
}
output.Flush();
}
// Guest names must not split one trace event across multiple output lines.
private static string Quote(string? value) => (value ?? "none")
.Replace("\\", "\\\\").Replace("'", "\\'").Replace("\r", "\\r").Replace("\n", "\\n");
}

View File

@ -274,7 +274,7 @@ internal static unsafe class PosixHostStubs
// Linux: call pthread_getspecific, preserving the registers that are
// volatile in SysV but non-volatile in Win64 (rsi, rdi).
var pthreadGetSpecific = NativeLibrary.GetExport(NativeLibrary.Load("libpthread.so.0"), "pthread_getspecific");
var pthreadGetSpecific = ResolveLibcExport("pthread_getspecific");
Emit(page, ref offset, 0x56); // push rsi
Emit(page, ref offset, 0x57); // push rdi
Emit(page, ref offset, 0x48, 0x83, 0xEC, 0x08); // sub rsp, 8
@ -366,25 +366,25 @@ internal static unsafe class PosixHostStubs
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
private static extern int pthread_key_create_mac(nuint* key, nint destructor);
[DllImport("libpthread.so.0", EntryPoint = "pthread_key_create", SetLastError = true)]
[DllImport("libc", EntryPoint = "pthread_key_create", SetLastError = true)]
private static extern int pthread_key_create_linux(uint* key, nint destructor);
[DllImport("libc", EntryPoint = "pthread_key_delete")]
private static extern int pthread_key_delete_mac(nuint key);
[DllImport("libpthread.so.0", EntryPoint = "pthread_key_delete")]
[DllImport("libc", EntryPoint = "pthread_key_delete")]
private static extern int pthread_key_delete_linux(uint key);
[DllImport("libc", EntryPoint = "pthread_setspecific")]
private static extern int pthread_setspecific_mac(nuint key, nint value);
[DllImport("libpthread.so.0", EntryPoint = "pthread_setspecific")]
[DllImport("libc", EntryPoint = "pthread_setspecific")]
private static extern int pthread_setspecific_linux(uint key, nint value);
[DllImport("libc", EntryPoint = "pthread_getspecific")]
private static extern nint pthread_getspecific_mac(nuint key);
[DllImport("libpthread.so.0", EntryPoint = "pthread_getspecific")]
[DllImport("libc", EntryPoint = "pthread_getspecific")]
private static extern nint pthread_getspecific_linux(uint key);
[DllImport("libc")]

View File

@ -0,0 +1,115 @@
// 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;
}
}

View File

@ -16,8 +16,6 @@ internal static class WindowsFaultCodes
public const uint FastFail = 0xC0000409u; // 3221226505
public const uint StackOverflow = 0xC00000FDu;
public const uint ClrManagedException = 0xE0434352u;
public const uint DebugOutputAnsi = 0x40010006u;
public const uint DebugOutputUnicode = 0x4001000Au;
// EXCEPTION_RECORD.ExceptionInformation[0] for access violations.
public const ulong AccessRead = 0;

View File

@ -47,8 +47,7 @@ internal sealed unsafe partial class WindowsFaultHandling : IHostFaultHandling
// FastFail (0xC0000409) is logged from this native path only: managed VEH never
// sees it (tLT18–21 silent exits after TBB AV recovery).
ReadOnlySpan<uint> nonManagedExceptionCodes =
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow,
WindowsFaultCodes.DebugOutputAnsi, WindowsFaultCodes.DebugOutputUnicode];
[WindowsFaultCodes.ClrManagedException, 0xE06D7363u, WindowsFaultCodes.FastFail, WindowsFaultCodes.StackOverflow];
EmitByte(code, ref offset, 0x48); EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x01); // mov rax, [rcx] (ExceptionRecord*)
EmitByte(code, ref offset, 0x8B); EmitByte(code, ref offset, 0x00); // mov eax, [rax] (ExceptionCode)
var passJumpOffsets = stackalloc int[nonManagedExceptionCodes.Length];

View File

@ -18,8 +18,6 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
public ICpuMemory Inner => _inner;
public string DescribeReadRange(ulong address, ulong size) => _inner.DescribeReadRange(address, size);
public bool TryRead(ulong virtualAddress, Span<byte> destination)
{
var result = _inner.TryRead(virtualAddress, destination);
@ -42,20 +40,9 @@ public sealed class TrackedCpuMemory : ICpuMemory, ITrackedCpuMemory, IGuestMemo
return result;
}
public bool TryCompare(
ulong virtualAddress,
ReadOnlySpan<byte> expected,
out bool equal) =>
_inner.TryCompare(virtualAddress, expected, out equal);
public bool TryCopy(ulong destinationAddress, ulong sourceAddress, ulong length) =>
_inner.TryCopy(destinationAddress, sourceAddress, length);
public bool CanRead(ulong address, ulong size) => _inner.CanRead(address, size);
public bool TryScanCString(ulong address, byte needle, bool findLast, ulong maxLength, out ulong match) =>
_inner.TryScanCString(address, needle, findLast, maxLength, out match);
public bool TryAllocateGuestMemory(ulong size, ulong alignment, out ulong address)
{
if (_inner is IGuestMemoryAllocator allocator)

View File

@ -1,122 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Reflection.PortableExecutable;
using System.Text;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Core.Diagnostics;
public static partial class WindowsCrashCapture
{
internal sealed class DebuggerBreakTracker
{
private readonly Dictionary<uint, nint> _threadStarts = new();
private nint _libraryBase;
private nint _breakThreadEntry;
private nint _breakpointAddress;
internal void Observe(DebugEvent debugEvent)
{
switch (debugEvent.Kind)
{
case CreateProcessEvent:
_threadStarts[debugEvent.ThreadId] = debugEvent.ProcessThreadStartAddress;
break;
case CreateThreadEvent:
_threadStarts[debugEvent.ThreadId] = debugEvent.ThreadStartAddress;
break;
case ExitThreadEvent:
_threadStarts.Remove(debugEvent.ThreadId);
break;
case LoadLibraryEvent:
if (TryReadDebuggerExports(debugEvent.FileHandle, out var threadOffset, out var breakpointOffset))
{
_libraryBase = debugEvent.LoadedLibraryBase;
_breakThreadEntry = checked(_libraryBase + threadOffset);
_breakpointAddress = checked(_libraryBase + breakpointOffset);
}
break;
case UnloadLibraryEvent when debugEvent.UnloadedLibraryBase == _libraryBase:
_libraryBase = _breakThreadEntry = _breakpointAddress = 0;
break;
}
}
internal bool IsDebuggerBreak(DebugEvent debugEvent) =>
debugEvent.Kind == ExceptionEvent && debugEvent.FirstChance != 0 &&
debugEvent.Exception.Code == BreakpointException && _libraryBase != 0 &&
debugEvent.Exception.Address == _breakpointAddress &&
_threadStarts.TryGetValue(debugEvent.ThreadId, out var startAddress) && startAddress == _breakThreadEntry;
}
private static unsafe bool TryReadDebuggerExports(nint fileHandle, out int threadOffset, out int breakpointOffset)
{
threadOffset = breakpointOffset = 0;
if (fileHandle == 0 || fileHandle == -1)
return false;
const int pathCapacity = 512;
var pathBuffer = stackalloc char[pathCapacity];
var pathLength = GetFinalPathNameByHandle(fileHandle, pathBuffer, pathCapacity, 0);
if (pathLength == 0 || pathLength >= pathCapacity)
return false;
var path = new string(pathBuffer, 0, (int)pathLength);
var systemLibrary = @"\\?\" + Path.Combine(Environment.SystemDirectory, "ntdll.dll");
if (!string.Equals(path, systemLibrary, StringComparison.OrdinalIgnoreCase))
return false;
try
{
// Read the file supplied by the debug event, then use the target's load address.
using var handle = new SafeFileHandle(fileHandle, ownsHandle: false);
using var stream = new FileStream(handle, FileAccess.Read);
using var image = new PEReader(stream);
if (image.PEHeaders.PEHeader is not { Magic: PEMagic.PE32Plus } header)
return false;
var directory = header.ExportTableDirectory;
if (directory.Size < 40)
return false;
var exports = image.GetSectionData(directory.RelativeVirtualAddress).GetReader(0, 40);
exports.Offset = 20;
var functionCount = exports.ReadInt32();
var nameCount = exports.ReadInt32();
var functionsAddress = exports.ReadInt32();
var namesAddress = exports.ReadInt32();
var ordinalsAddress = exports.ReadInt32();
var functions = image.GetSectionData(functionsAddress).GetReader(0, checked(functionCount * 4));
var names = image.GetSectionData(namesAddress).GetReader(0, checked(nameCount * 4));
var ordinals = image.GetSectionData(ordinalsAddress).GetReader(0, checked(nameCount * 2));
for (var nameIndex = 0; nameIndex < nameCount; nameIndex++)
{
var nameBlock = image.GetSectionData(names.ReadInt32());
var nameBytes = nameBlock.GetContent(0, Math.Min(nameBlock.Length, "DbgUiRemoteBreakin".Length + 1)).AsSpan();
var terminator = nameBytes.IndexOf((byte)0);
var ordinal = ordinals.ReadUInt16();
if (ordinal >= functionCount)
return false;
if (terminator < 0)
continue;
var name = Encoding.ASCII.GetString(nameBytes[..terminator]);
if (name is not ("DbgUiRemoteBreakin" or "DbgBreakPoint"))
continue;
functions.Offset = ordinal * 4;
var functionOffset = functions.ReadInt32();
// A forwarded export is not executable code in this image.
if (functionOffset <= 0 || functionOffset >= header.SizeOfImage ||
(functionOffset >= directory.RelativeVirtualAddress &&
functionOffset - directory.RelativeVirtualAddress < directory.Size))
return false;
if (name == "DbgUiRemoteBreakin")
threadOffset = functionOffset;
else
breakpointOffset = functionOffset;
}
return threadOffset != 0 && breakpointOffset != 0;
}
catch (Exception exception) when (exception is IOException or BadImageFormatException or ArgumentException or OverflowException)
{
threadOffset = breakpointOffset = 0;
return false;
}
}
}

View File

@ -1,161 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Core.Diagnostics;
public static partial class WindowsCrashCapture
{
private const uint ExceptionEvent = 1;
private const uint CreateThreadEvent = 2;
private const uint CreateProcessEvent = 3;
private const uint ExitThreadEvent = 4;
private const uint ExitProcessEvent = 5;
private const uint LoadLibraryEvent = 6;
private const uint UnloadLibraryEvent = 7;
private const uint BreakpointException = 0x80000003;
private const uint DebugContinue = 0x00010002;
private const uint DebugExceptionNotHandled = 0x80010001;
private const uint ProcessReadAccess = 0x0450;
private const uint ThreadContextAccess = 0x0008;
private const uint DumpFullMemory = 0x00000002;
private const uint DumpHandleData = 0x00000004;
private const uint DumpUnloadedModules = 0x00000020;
private const uint DumpMemoryInformation = 0x00000800;
private const uint DumpThreadInformation = 0x00001000;
private const uint DumpIgnoreInaccessibleMemory = 0x00020000;
private const uint FullDumpFlags = DumpFullMemory | DumpHandleData | DumpUnloadedModules |
DumpMemoryInformation | DumpThreadInformation | DumpIgnoreInaccessibleMemory;
private const uint AllX64ContextFlags = 0x0010001F;
private static unsafe void WriteDump(uint processId, DebugEvent debugEvent, string dumpPath)
{
using var process = OpenProcess(ProcessReadAccess, false, processId);
if (process.IsInvalid)
throw NativeFailure("Open the crashed process");
using var thread = OpenThread(ThreadContextAccess, false, debugEvent.ThreadId);
if (thread.IsInvalid)
throw NativeFailure("Open the faulting thread");
var context = (ThreadContext*)NativeMemory.AlignedAlloc((nuint)sizeof(ThreadContext), 16);
if (context == null)
throw new OutOfMemoryException();
try
{
NativeMemory.Clear(context, (nuint)sizeof(ThreadContext));
context->Flags = AllX64ContextFlags;
if (!GetThreadContext(thread, context))
throw NativeFailure("Read the faulting thread context");
// The exception pointers refer to this helper, not to the crashed process.
var record = debugEvent.Exception;
record.NestedRecord = 0;
var pointers = new ExceptionPointers { Record = &record, Context = context };
var exception = new DumpExceptionInformation
{
ThreadId = debugEvent.ThreadId,
Pointers = &pointers,
ClientPointers = 0,
};
using var output = new FileStream(dumpPath, FileMode.CreateNew, FileAccess.Write, FileShare.Read);
if (!MiniDumpWriteDump(process, processId, output.SafeFileHandle, FullDumpFlags, &exception, 0, 0))
throw NativeFailure("Write the crash dump");
output.Flush(flushToDisk: true);
}
finally
{
NativeMemory.AlignedFree(context);
}
}
[StructLayout(LayoutKind.Explicit, Size = 176)]
internal struct DebugEvent
{
[FieldOffset(0)] public uint Kind;
[FieldOffset(4)] public uint ProcessId;
[FieldOffset(8)] public uint ThreadId;
[FieldOffset(16)] public ExceptionRecord Exception;
[FieldOffset(168)] public uint FirstChance;
[FieldOffset(16)] public nint FileHandle;
[FieldOffset(16)] public uint ExitCode;
[FieldOffset(24)] public nint LoadedLibraryBase;
[FieldOffset(16)] public nint UnloadedLibraryBase;
[FieldOffset(32)] public nint ThreadStartAddress;
[FieldOffset(64)] public nint ProcessThreadStartAddress;
}
[StructLayout(LayoutKind.Sequential)]
internal unsafe struct ExceptionRecord
{
public uint Code;
public uint Flags;
public nint NestedRecord;
public nint Address;
public uint ParameterCount;
public fixed ulong Parameters[15];
}
[StructLayout(LayoutKind.Explicit, Size = 1232)]
internal struct ThreadContext
{
[FieldOffset(48)] public uint Flags;
}
[StructLayout(LayoutKind.Sequential)]
private unsafe struct ExceptionPointers
{
public ExceptionRecord* Record;
public ThreadContext* Context;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
internal unsafe struct DumpExceptionInformation
{
public uint ThreadId;
public void* Pointers;
public int ClientPointers;
}
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DebugActiveProcess(uint processId);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DebugActiveProcessStop(uint processId);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool DebugSetProcessKillOnExit([MarshalAs(UnmanagedType.Bool)] bool killOnExit);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool WaitForDebugEventEx(out DebugEvent debugEvent, uint milliseconds);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool ContinueDebugEvent(uint processId, uint threadId, uint status);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial SafeProcessHandle OpenProcess(uint access, [MarshalAs(UnmanagedType.Bool)] bool inherit, uint processId);
[LibraryImport("kernel32.dll", SetLastError = true)]
private static partial SafeWaitHandle OpenThread(uint access, [MarshalAs(UnmanagedType.Bool)] bool inherit, uint threadId);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static unsafe partial bool GetThreadContext(SafeWaitHandle thread, ThreadContext* context);
[LibraryImport("kernel32.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
private static partial bool CloseHandle(nint handle);
[LibraryImport("kernel32.dll", EntryPoint = "GetFinalPathNameByHandleW", SetLastError = true)]
private static unsafe partial uint GetFinalPathNameByHandle(nint file, char* path, uint length, uint flags);
[LibraryImport("dbghelp.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
private static unsafe partial bool MiniDumpWriteDump(SafeProcessHandle process, uint processId,
SafeFileHandle file, uint dumpType, DumpExceptionInformation* exception, nint userStream, nint callback);
}

View File

@ -1,182 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.ComponentModel;
using System.Diagnostics;
using System.Globalization;
using System.Reflection;
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
namespace SharpEmu.Core.Diagnostics;
public static partial class WindowsCrashCapture
{
public const string HelperArgument = "--sharpemu-crash-helper";
private const string CaptureVariable = "SHARPEMU_CRASH_CAPTURE";
public static void StartIfEnabled(string? logFilePath)
{
if (!string.Equals(Environment.GetEnvironmentVariable(CaptureVariable), "1", StringComparison.Ordinal))
return;
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
{
Console.Error.WriteLine("[CRASH][WARN] Crash capture requires a Windows x64 process.");
return;
}
try
{
var logPath = string.IsNullOrWhiteSpace(logFilePath)
? Path.Combine(AppContext.BaseDirectory, "user", "logs", $"run-{DateTime.UtcNow:yyyyMMdd-HHmmss}.log")
: Path.GetFullPath(logFilePath);
var dumpPath = Path.ChangeExtension(logPath, $"crash-{Environment.ProcessId}.dmp");
Directory.CreateDirectory(Path.GetDirectoryName(dumpPath)!);
var readyEventName = $"Local\\SharpEmu-Crash-{Guid.NewGuid():N}";
using var readyEvent = new EventWaitHandle(false, EventResetMode.ManualReset, readyEventName);
var executable = Environment.ProcessPath ?? throw new InvalidOperationException("The executable path is not available.");
var startInfo = new ProcessStartInfo(executable) { UseShellExecute = false, CreateNoWindow = true };
if (string.Equals(Path.GetFileNameWithoutExtension(executable), "dotnet", StringComparison.OrdinalIgnoreCase))
startInfo.ArgumentList.Add(Assembly.GetEntryAssembly()!.Location);
startInfo.ArgumentList.Add(HelperArgument);
startInfo.ArgumentList.Add(Environment.ProcessId.ToString(CultureInfo.InvariantCulture));
startInfo.ArgumentList.Add(readyEventName);
startInfo.ArgumentList.Add(dumpPath);
startInfo.Environment.Remove(CaptureVariable);
// The helper stays attached until this process exits, including runtime teardown.
using var helper = Process.Start(startInfo) ?? throw new InvalidOperationException("The crash helper did not start.");
using var exited = new ProcessExitWaitHandle(helper);
WaitForHelper(readyEvent, exited);
Console.Error.WriteLine($"[CRASH][INFO] Crash capture ready. Dump: {dumpPath}");
Console.Error.WriteLine($"[CRASH][INFO] Capture report: {dumpPath}.capture.log");
Console.Error.WriteLine($"[CRASH][INFO] Runtime: {RuntimeInformation.FrameworkDescription}. Debugging can change timing.");
}
catch (Exception exception)
{
Console.Error.WriteLine($"[CRASH][ERROR] Crash capture is not active: {exception.Message}");
}
}
internal static void WaitForHelper(WaitHandle ready, WaitHandle exited)
{
if (WaitHandle.WaitAny([ready, exited]) != 0)
throw new InvalidOperationException("The crash helper exited before capture was ready. Check the capture report.");
}
private sealed class ProcessExitWaitHandle : WaitHandle
{
public ProcessExitWaitHandle(Process process) =>
SafeWaitHandle = new SafeWaitHandle(process.Handle, ownsHandle: false);
}
public static int RunHelper(string[] arguments)
{
if (!OperatingSystem.IsWindows() || RuntimeInformation.ProcessArchitecture != Architecture.X64 ||
arguments.Length != 4 || arguments[0] != HelperArgument ||
!uint.TryParse(arguments[1], NumberStyles.None, CultureInfo.InvariantCulture, out var processId) || processId == 0)
return 1;
var attached = false;
StreamWriter? report = null;
try
{
report = new StreamWriter(arguments[3] + ".capture.log", append: true) { AutoFlush = true };
report.WriteLine($"Capture helper started. Process: {processId}. Dump: {arguments[3]}");
report.WriteLine($"Runtime: {RuntimeInformation.FrameworkDescription}. OS: {Environment.OSVersion}.");
using var readyEvent = EventWaitHandle.OpenExisting(arguments[2]);
if (!DebugActiveProcess(processId))
throw NativeFailure("Attach to the game process");
attached = true;
if (!DebugSetProcessKillOnExit(false))
throw NativeFailure("Keep the game alive if the helper exits");
return ProcessDebugEvents(processId, arguments[3], readyEvent, report);
}
catch (Exception exception)
{
report?.WriteLine($"Capture failed: {exception.Message}");
Console.Error.WriteLine($"[CRASH][ERROR] Capture failed: {exception.Message}");
return 1;
}
finally
{
if (attached)
_ = DebugActiveProcessStop(processId);
report?.Dispose();
}
}
private static int ProcessDebugEvents(uint processId, string dumpPath, EventWaitHandle readyEvent, TextWriter report)
{
var initialBreakpoint = true;
var captured = false;
var debuggerBreaks = new DebuggerBreakTracker();
while (true)
{
if (!WaitForDebugEventEx(out var debugEvent, uint.MaxValue))
throw NativeFailure("Wait for a debug event");
var continuation = DebugContinue;
var ready = false;
try
{
try
{
debuggerBreaks.Observe(debugEvent);
}
finally
{
if (debugEvent.Kind is CreateProcessEvent or LoadLibraryEvent && debugEvent.FileHandle != 0)
_ = CloseHandle(debugEvent.FileHandle);
}
if (debugEvent.Kind == ExceptionEvent)
{
continuation = DebugExceptionNotHandled;
if (initialBreakpoint && debugEvent.FirstChance != 0 && debugEvent.Exception.Code == BreakpointException)
{
initialBreakpoint = false;
continuation = DebugContinue;
ready = true;
}
else if (debuggerBreaks.IsDebuggerBreak(debugEvent))
{
continuation = DebugContinue;
report.WriteLine($"Debugger break continued. Thread: {debugEvent.ThreadId}.");
}
else if (debugEvent.FirstChance == 0 && !captured)
{
captured = true;
report.WriteLine($"Unhandled exception: 0x{debugEvent.Exception.Code:X8}. " +
$"Address: 0x{debugEvent.Exception.Address:X16}. Thread: {debugEvent.ThreadId}.");
WriteDump(processId, debugEvent, dumpPath);
report.WriteLine("Dump complete.");
}
}
}
finally
{
// Even a failed dump must leave the original exception unhandled.
if (!ContinueDebugEvent(debugEvent.ProcessId, debugEvent.ThreadId, continuation))
throw NativeFailure("Continue a debug event");
}
if (ready)
{
report.WriteLine("Capture ready. Application exceptions remain unhandled by the helper.");
readyEvent.Set();
}
if (debugEvent.Kind == ExitProcessEvent)
{
report.WriteLine($"Process exited: 0x{debugEvent.ExitCode:X8}. Dump captured: {captured}.");
return 0;
}
}
}
private static Win32Exception NativeFailure(string operation)
{
var error = Marshal.GetLastPInvokeError();
return new Win32Exception(error, $"{operation} failed (Windows error {error}).");
}
}

File diff suppressed because it is too large Load Diff

View File

@ -1,66 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.InteropServices;
using Iced.Intel;
namespace SharpEmu.Core.Loader;
// Use two 128-bit stores to let Rosetta report a memory fault.
// Keep the source register, general registers, and flags unchanged.
internal static class RosettaVectorStorePatch
{
internal static bool IsRequired { get; } = IsRosettaProcess();
private static unsafe bool IsRosettaProcess()
{
if (!OperatingSystem.IsMacOS() || RuntimeInformation.ProcessArchitecture != Architecture.X64)
{
return false;
}
int translationStatus = 0;
nuint valueByteCount = sizeof(int);
return ReadSystemControlValue("sysctl.proc_translated", &translationStatus, &valueByteCount, null, 0) == 0 && translationStatus == 1;
}
[DllImport("libSystem.B.dylib", EntryPoint = "sysctlbyname")]
private static extern unsafe int ReadSystemControlValue(string controlName, void* valueBuffer, nuint* valueByteCount, void* newValueBuffer, nuint newValueByteCount);
internal static bool RequiresStoreSplit(in Instruction instruction) =>
instruction.Code is Code.VEX_Vmovups_ymmm256_ymm or Code.VEX_Vmovupd_ymmm256_ymm or Code.VEX_Vmovdqu_ymmm256_ymm &&
instruction.Op0Kind == OpKind.Memory && instruction.Op1Register is >= Register.YMM0 and <= Register.YMM15;
internal static IList<Instruction> SplitVectorStores(IList<Instruction> instructions)
{
var splitInstructions = new List<Instruction>(instructions.Count * 2);
foreach (var instruction in instructions)
{
if (!RequiresStoreSplit(instruction))
{
splitInstructions.Add(instruction);
continue;
}
var lowerStore = instruction;
lowerStore.Code = Code.VEX_Vmovups_xmmm128_xmm;
lowerStore.Op1Register = Register.XMM0 + (instruction.Op1Register - Register.YMM0);
splitInstructions.Add(lowerStore);
var upperStore = instruction;
upperStore.Code = Code.VEX_Vextractf128_xmmm128_ymm_imm8;
upperStore.Op2Kind = OpKind.Immediate8;
upperStore.Immediate8 = 1;
upperStore.MemoryDisplacement64 = unchecked(instruction.MemoryDisplacement64 + 16);
// Use the address size for the displacement size that Iced requires.
upperStore.MemoryDisplSize = instruction.MemoryBase is >= Register.EAX and <= Register.R15D ||
instruction.MemoryIndex is >= Register.EAX and <= Register.R15D ? 4 : 8;
// Give the added instruction a separate address for the block encoder.
// Keep branch targets at the first store.
upperStore.IP = instruction.IP + 1;
splitInstructions.Add(upperStore);
}
return splitInstructions;
}
}

View File

@ -27,8 +27,7 @@ public sealed class SelfImage
string? version = null,
uint tlsModuleId = 0,
ulong tlsMemorySize = 0,
ulong tlsStaticOffset = 0,
IReadOnlyList<string>? importedModuleNames = null)
ulong tlsStaticOffset = 0)
{
ArgumentNullException.ThrowIfNull(programHeaders);
ArgumentNullException.ThrowIfNull(mappedRegions);
@ -51,7 +50,6 @@ public sealed class SelfImage
TlsModuleId = tlsModuleId;
TlsMemorySize = tlsMemorySize;
TlsStaticOffset = tlsStaticOffset;
ImportedModuleNames = importedModuleNames ?? Array.Empty<string>();
}
public bool IsSelf { get; }
@ -90,6 +88,4 @@ public sealed class SelfImage
/// <summary>Variant II distance from the thread pointer to this module's static TLS base.</summary>
public ulong TlsStaticOffset { get; }
public IReadOnlyList<string> ImportedModuleNames { get; }
}

View File

@ -56,8 +56,6 @@ public sealed class SelfLoader : ISelfLoader
private const long DtSceStrSize = 0x61000037;
private const long DtSceSymTab = 0x61000039;
private const long DtSceSymTabSize = 0x6100003F;
private const long DtSceNeededModule = 0x6100000F;
private const long DtSceNeededModuleNext = 0x61000045;
private const uint RelocationTypeNone = 0;
private const uint RelocationTypeAbsolute64 = 1;
@ -171,7 +169,6 @@ public sealed class SelfLoader : ISelfLoader
if (clearVirtualMemory)
{
SharpEmu.Libs.Kernel.KernelMemoryCompatExports.ResetBackingMappings(virtualMemory as IGuestBackedSpace);
virtualMemory.Clear();
_nextTlsModuleId = 1;
GuestTlsTemplate.Reset();
@ -262,14 +259,7 @@ public sealed class SelfLoader : ISelfLoader
imageBase,
_moduleManager,
tlsModuleId,
out var importedRelocations,
out var importedModuleNames);
if (tlsModuleId != 0)
{
// Thread copies must include pointers written by relocation.
GuestTlsTemplate.UpdateInitializationImage(
tlsModuleId, ReadTlsInitializationImage(processTlsHeader, virtualMemory, imageBase));
}
out var importedRelocations);
var effectiveImportStubs = importStubs.Count == 0
? new Dictionary<ulong, string>()
: new Dictionary<ulong, string>(importStubs);
@ -300,16 +290,6 @@ public sealed class SelfLoader : ISelfLoader
out var initializerFunctions);
var procParamAddress = ResolveProcParamAddress(programHeaders, imageBase);
if (virtualMemory is PhysicalVirtualMemory patchableMemory)
{
_ = GuestRedZonePatcher.Patch(
virtualMemory,
patchableMemory,
programHeaders,
imageBase,
totalImageSize);
}
Console.WriteLine($"[LOADER] ELF e_entry: 0x{elfHeader.EntryPoint:X16}");
Console.WriteLine($"[LOADER] Generation: {(isNextGen ? "Gen5 (PS5)" : "Gen4 (PS4)")}");
Console.WriteLine($"[LOADER] Using image base: 0x{imageBase:X16}");
@ -351,8 +331,7 @@ public sealed class SelfLoader : ISelfLoader
applicationInfo.Version,
tlsModuleId,
tlsInfo.MemorySize,
tlsInfo.StaticOffset,
importedModuleNames);
tlsInfo.StaticOffset);
}
private static (string? Title, string? TitleId, string? Version) TryLoadParamJson(
@ -560,7 +539,18 @@ public sealed class SelfLoader : ISelfLoader
return default;
}
var initImage = ReadTlsInitializationImage(tlsHeader, virtualMemory, imageBase);
// tdata (initialized) bytes come from the mapped segment; tbss is the
// implicitly-zero remainder up to MemorySize.
var fileSize = (int)Math.Min(tlsHeader.FileSize, tlsHeader.MemorySize);
var initImage = fileSize > 0 ? new byte[fileSize] : [];
if (fileSize > 0 &&
!virtualMemory.TryRead(imageBase + tlsHeader.VirtualAddress, initImage))
{
Console.Error.WriteLine(
$"[LOADER][TLS] Failed to read TLS init image at 0x{imageBase + tlsHeader.VirtualAddress:X}; seeding zeros.");
initImage = [];
}
var staticOffset = GuestTlsTemplate.RegisterModule(
tlsModuleId,
initImage,
@ -574,19 +564,6 @@ public sealed class SelfLoader : ISelfLoader
return new ModuleTlsInfo(tlsHeader.MemorySize, staticOffset);
}
private static byte[] ReadTlsInitializationImage(
ProgramHeader header, IVirtualMemory virtualMemory, ulong imageBase)
{
if (header.FileSize > header.MemorySize || header.FileSize > int.MaxValue)
throw new InvalidDataException("PT_TLS initialization size is invalid.");
var initializationImage = new byte[(int)header.FileSize];
var address = checked(imageBase + header.VirtualAddress);
if (initializationImage.Length != 0 && !virtualMemory.TryRead(address, initializationImage))
throw new InvalidDataException($"Cannot read TLS initialization bytes at 0x{address:X}.");
return initializationImage;
}
private static IReadOnlyDictionary<ulong, string> ResolveAndPatchImportStubs(
ReadOnlySpan<byte> imageData,
LoadContext loadContext,
@ -596,11 +573,9 @@ public sealed class SelfLoader : ISelfLoader
ulong imageBase,
IModuleManager? moduleManager,
uint tlsModuleId,
out IReadOnlyList<ImportedSymbolRelocation> importedRelocations,
out IReadOnlyList<string> importedModuleNames)
out IReadOnlyList<ImportedSymbolRelocation> importedRelocations)
{
importedRelocations = Array.Empty<ImportedSymbolRelocation>();
importedModuleNames = Array.Empty<string>();
if (!TryGetProgramHeader(programHeaders, ProgramHeaderType.Dynamic, out var dynamicHeader, out var dynamicHeaderIndex))
{
return EmptyImportStubs;
@ -691,21 +666,15 @@ public sealed class SelfLoader : ISelfLoader
byte[] stringTable = Array.Empty<byte>();
byte[] symbolTable = Array.Empty<byte>();
if (dynamicInfo.StrTabOffset != 0 && dynamicInfo.StrTabSize != 0 &&
!TryLoadTableBytes(
elfData,
virtualMemory,
imageBase,
dynamicInfo.StrTabOffset,
dynamicInfo.StrTabSize,
out stringTable))
{
return EmptyImportStubs;
}
if (maxSymbolIndex != 0)
{
if (stringTable.Length == 0)
if (!TryLoadTableBytes(
elfData,
virtualMemory,
imageBase,
dynamicInfo.StrTabOffset,
dynamicInfo.StrTabSize,
out stringTable))
{
return EmptyImportStubs;
}
@ -725,8 +694,6 @@ public sealed class SelfLoader : ISelfLoader
}
}
importedModuleNames = ReadImportedModuleNames(dynamicTable, stringTable);
var descriptors = new List<RelocationDescriptor>(256);
var orderedImportNids = new List<string>(128);
var seenImportNids = new HashSet<string>(StringComparer.Ordinal);
@ -2018,39 +1985,6 @@ public sealed class SelfLoader : ISelfLoader
preInitArraySize);
}
private static IReadOnlyList<string> ReadImportedModuleNames(
ReadOnlySpan<byte> dynamicTable,
ReadOnlySpan<byte> stringTable)
{
if (stringTable.IsEmpty)
{
return Array.Empty<string>();
}
var names = new List<string>();
for (var offset = 0; offset + DynamicEntrySize <= dynamicTable.Length; offset += DynamicEntrySize)
{
var tag = BinaryPrimitives.ReadInt64LittleEndian(dynamicTable.Slice(offset, sizeof(long)));
var value = BinaryPrimitives.ReadUInt64LittleEndian(dynamicTable.Slice(offset + sizeof(long), sizeof(ulong)));
if (tag == DtNull)
{
break;
}
if (tag is not (DtSceNeededModule or DtSceNeededModuleNext) ||
!TryReadNullTerminatedAscii(stringTable, (uint)value, out var name) ||
string.IsNullOrWhiteSpace(name) ||
names.Contains(name, StringComparer.OrdinalIgnoreCase))
{
continue;
}
names.Add(name);
}
return names.Count == 0 ? Array.Empty<string>() : names;
}
private static bool IsSupportedRelocationType(uint relocationType)
{
return relocationType is

View File

@ -1,405 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Runtime.Intrinsics.X86;
using Iced.Intel;
using static Iced.Intel.AssemblerRegisters;
namespace SharpEmu.Core.Loader;
// Rewrite SHA extension instructions into general-register code when the host CPU lacks them.
// Rosetta 2 and some x86 hosts raise #UD for them, and a signal per instruction is far too slow
// for titles that hash whole files. The expansion keeps every register except the destination
// XMM register and keeps the flags. It runs inside the trampoline, after the red zone is skipped.
internal static class ShaInstructionRewrite
{
// pushfq plus the saved general registers.
private const int SavedBytes = 8 * 13;
// Scratch layout: source 1 (and result), source 2, XMM0 (SHA256RNDS2 round inputs).
private const int Source1 = 0;
private const int Source2 = 16;
private const int Xmm0Copy = 32;
private const int ScratchBytes = 48;
private static readonly Register[] SavedRegisters =
{
Register.RAX, Register.RCX, Register.RDX, Register.RBX, Register.RSI, Register.RDI,
Register.R8, Register.R9, Register.R10, Register.R11, Register.R12, Register.R13,
};
internal static bool IsRequired { get; } = HostLacksSha() &&
!string.Equals(Environment.GetEnvironmentVariable("SHARPEMU_DISABLE_SHA_REWRITE"), "1", StringComparison.Ordinal);
// Bytes the expansion of one instruction needs at most in the trampoline.
internal const int MaximumExpansionBytes = 1024;
private static bool HostLacksSha()
{
if (!X86Base.IsSupported)
{
return false;
}
var (_, ebx, _, _) = X86Base.CpuId(7, 0);
return (ebx & (1 << 29)) == 0;
}
internal static bool IsShaInstruction(in Instruction instruction) => instruction.Mnemonic is
Mnemonic.Sha1rnds4 or Mnemonic.Sha1nexte or Mnemonic.Sha1msg1 or Mnemonic.Sha1msg2 or
Mnemonic.Sha256rnds2 or Mnemonic.Sha256msg1 or Mnemonic.Sha256msg2;
// A memory source with a segment prefix or an RIP-relative address cannot be rebuilt here.
internal static bool CanRewrite(in Instruction instruction)
{
if (!IsShaInstruction(instruction) || instruction.Op0Kind != OpKind.Register)
{
return false;
}
return instruction.Op1Kind == OpKind.Register ||
(instruction.Op1Kind == OpKind.Memory &&
instruction.SegmentPrefix == Register.None &&
!instruction.IsIPRelativeMemoryOperand);
}
internal static IList<Instruction> Expand(IList<Instruction> instructions)
{
var expanded = new List<Instruction>(instructions.Count);
foreach (var instruction in instructions)
{
if (!CanRewrite(instruction))
{
expanded.Add(instruction);
continue;
}
var assembler = new Assembler(64);
EmitExpansion(assembler, instruction);
var first = true;
ulong generatedIp = 0xFFFF_0000_0000_0000UL + ((instruction.IP & 0xFFFF_FFFF) << 12);
foreach (var generated in assembler.Instructions)
{
var copy = generated;
// Keep branch targets at the first generated instruction.
copy.IP = first ? instruction.IP : generatedIp++;
first = false;
expanded.Add(copy);
}
}
return expanded;
}
internal static void EmitExpansion(Assembler a, in Instruction instruction)
{
var destination = ToXmm(instruction.Op0Register);
a.pushfq();
foreach (var register in SavedRegisters)
{
a.push(ToGpr64(register));
}
a.sub(rsp, ScratchBytes);
if (instruction.Op1Kind == OpKind.Memory)
{
// LEA reads the guest registers before any of them change.
a.lea(rax, RebaseMemory(instruction));
a.mov(rcx, __qword_ptr[rax]);
a.mov(__qword_ptr[rsp + Source2], rcx);
a.mov(rcx, __qword_ptr[rax + 8]);
a.mov(__qword_ptr[rsp + Source2 + 8], rcx);
}
else
{
a.movdqu(__xmmword_ptr[rsp + Source2], ToXmm(instruction.Op1Register));
}
a.movdqu(__xmmword_ptr[rsp + Source1], destination);
if (instruction.Mnemonic == Mnemonic.Sha256rnds2)
{
a.movdqu(__xmmword_ptr[rsp + Xmm0Copy], xmm0);
}
switch (instruction.Mnemonic)
{
case Mnemonic.Sha1rnds4: EmitSha1Rnds4(a, instruction.Immediate8 & 3); break;
case Mnemonic.Sha1nexte: EmitSha1Nexte(a); break;
case Mnemonic.Sha1msg1: EmitSha1Msg1(a); break;
case Mnemonic.Sha1msg2: EmitSha1Msg2(a); break;
case Mnemonic.Sha256rnds2: EmitSha256Rnds2(a); break;
case Mnemonic.Sha256msg1: EmitSha256Msg1(a); break;
default: EmitSha256Msg2(a); break;
}
a.movdqu(destination, __xmmword_ptr[rsp + Source1]);
a.add(rsp, ScratchBytes);
for (var index = SavedRegisters.Length - 1; index >= 0; index--)
{
a.pop(ToGpr64(SavedRegisters[index]));
}
a.popfq();
}
// The guest stack pointer is above the scratch area, the saved registers and the red zone.
private static AssemblerMemoryOperand RebaseMemory(in Instruction instruction)
{
var displacement = unchecked((long)instruction.MemoryDisplacement64);
if (instruction.MemoryBase == Register.RSP)
{
displacement += ScratchBytes + SavedBytes + 128;
}
var baseRegister = instruction.MemoryBase == Register.None ? default : ToGpr64(instruction.MemoryBase);
var memory = instruction.MemoryBase == Register.None ? __[displacement] : __[baseRegister + displacement];
if (instruction.MemoryIndex != Register.None)
{
memory = instruction.MemoryBase == Register.None
? __[ToGpr64(instruction.MemoryIndex) * instruction.MemoryIndexScale + displacement]
: __[baseRegister + ToGpr64(instruction.MemoryIndex) * instruction.MemoryIndexScale + displacement];
}
return memory;
}
private static AssemblerMemoryOperand Dword(int slot, int index) => __dword_ptr[rsp + slot + 4 * index];
private static void EmitSha1Rnds4(Assembler a, int function)
{
var k = function switch
{
0 => 0x5A827999,
1 => 0x6ED9EBA1,
2 => unchecked((int)0x8F1BBCDC),
_ => unchecked((int)0xCA62C1D6),
};
a.mov(eax, Dword(Source1, 3));
a.mov(ebx, Dword(Source1, 2));
a.mov(ecx, Dword(Source1, 1));
a.mov(edx, Dword(Source1, 0));
a.xor(esi, esi);
for (var round = 0; round < 4; round++)
{
switch (function)
{
case 0:
// (b & c) ^ (~b & d) == d ^ (b & (c ^ d))
a.mov(edi, ecx);
a.xor(edi, edx);
a.and(edi, ebx);
a.xor(edi, edx);
break;
case 2:
// (b & c) | (d & (b | c)) is the majority of b, c and d.
a.mov(edi, ebx);
a.or(edi, ecx);
a.and(edi, edx);
a.mov(r8d, ebx);
a.and(r8d, ecx);
a.or(edi, r8d);
break;
default:
a.mov(edi, ebx);
a.xor(edi, ecx);
a.xor(edi, edx);
break;
}
a.add(edi, Dword(Source2, 3 - round));
a.add(edi, esi);
a.add(edi, k);
a.mov(r8d, eax);
a.rol(r8d, (byte)5);
a.add(edi, r8d);
a.mov(esi, edx);
a.mov(edx, ecx);
a.mov(ecx, ebx);
a.rol(ecx, (byte)30);
a.mov(ebx, eax);
a.mov(eax, edi);
}
a.mov(Dword(Source1, 3), eax);
a.mov(Dword(Source1, 2), ebx);
a.mov(Dword(Source1, 1), ecx);
a.mov(Dword(Source1, 0), edx);
}
private static void EmitSha1Nexte(Assembler a)
{
a.mov(eax, Dword(Source1, 3));
a.rol(eax, (byte)30);
a.add(eax, Dword(Source2, 3));
a.mov(Dword(Source1, 3), eax);
for (var index = 0; index < 3; index++)
{
a.mov(ecx, Dword(Source2, index));
a.mov(Dword(Source1, index), ecx);
}
}
private static void EmitSha1Msg1(Assembler a)
{
// w0..w3 from source 1 (w0 in dword 3), w4 and w5 from source 2.
a.mov(eax, Dword(Source1, 3));
a.mov(ebx, Dword(Source1, 2));
a.mov(ecx, Dword(Source1, 1));
a.mov(edx, Dword(Source1, 0));
a.mov(esi, Dword(Source2, 3));
a.mov(edi, Dword(Source2, 2));
a.xor(eax, ecx);
a.xor(ebx, edx);
a.xor(ecx, esi);
a.xor(edx, edi);
a.mov(Dword(Source1, 3), eax);
a.mov(Dword(Source1, 2), ebx);
a.mov(Dword(Source1, 1), ecx);
a.mov(Dword(Source1, 0), edx);
}
private static void EmitSha1Msg2(Assembler a)
{
a.mov(eax, Dword(Source1, 3));
a.xor(eax, Dword(Source2, 2));
a.rol(eax, (byte)1);
a.mov(ebx, Dword(Source1, 2));
a.xor(ebx, Dword(Source2, 1));
a.rol(ebx, (byte)1);
a.mov(ecx, Dword(Source1, 1));
a.xor(ecx, Dword(Source2, 0));
a.rol(ecx, (byte)1);
a.mov(edx, Dword(Source1, 0));
a.xor(edx, eax);
a.rol(edx, (byte)1);
a.mov(Dword(Source1, 3), eax);
a.mov(Dword(Source1, 2), ebx);
a.mov(Dword(Source1, 1), ecx);
a.mov(Dword(Source1, 0), edx);
}
private static void EmitSha256Rnds2(Assembler a)
{
// a..h in eax, ebx, ecx, edx, esi, edi, r8d, r9d.
a.mov(eax, Dword(Source2, 3));
a.mov(ebx, Dword(Source2, 2));
a.mov(ecx, Dword(Source1, 3));
a.mov(edx, Dword(Source1, 2));
a.mov(esi, Dword(Source2, 1));
a.mov(edi, Dword(Source2, 0));
a.mov(r8d, Dword(Source1, 1));
a.mov(r9d, Dword(Source1, 0));
for (var round = 0; round < 2; round++)
{
// r10d = h + Sigma1(e) + Ch(e, f, g) + wk
EmitRotateXor(a, r10d, esi, r11d, 6, 11, 25);
a.add(r10d, r9d);
a.add(r10d, Dword(Xmm0Copy, round));
a.mov(r11d, edi);
a.xor(r11d, r8d);
a.and(r11d, esi);
a.xor(r11d, r8d);
a.add(r10d, r11d);
// r11d = Sigma0(a) + Maj(a, b, c)
EmitRotateXor(a, r11d, eax, r12d, 2, 13, 22);
a.mov(r12d, eax);
a.or(r12d, ebx);
a.and(r12d, ecx);
a.mov(r13d, eax);
a.and(r13d, ebx);
a.or(r12d, r13d);
a.add(r11d, r12d);
a.mov(r9d, r8d);
a.mov(r8d, edi);
a.mov(edi, esi);
a.mov(esi, edx);
a.add(esi, r10d);
a.mov(edx, ecx);
a.mov(ecx, ebx);
a.mov(ebx, eax);
a.mov(eax, r10d);
a.add(eax, r11d);
}
a.mov(Dword(Source1, 3), eax);
a.mov(Dword(Source1, 2), ebx);
a.mov(Dword(Source1, 1), esi);
a.mov(Dword(Source1, 0), edi);
}
private static void EmitSha256Msg1(Assembler a)
{
// w0..w3 in edx, ecx, ebx, eax; w4 in esi. Results go to r9d..r12d.
a.mov(eax, Dword(Source1, 3));
a.mov(ebx, Dword(Source1, 2));
a.mov(ecx, Dword(Source1, 1));
a.mov(edx, Dword(Source1, 0));
a.mov(esi, Dword(Source2, 0));
EmitSmallSigma(a, r9d, esi, r8d, 7, 18, 3);
a.add(r9d, eax);
EmitSmallSigma(a, r10d, eax, r8d, 7, 18, 3);
a.add(r10d, ebx);
EmitSmallSigma(a, r11d, ebx, r8d, 7, 18, 3);
a.add(r11d, ecx);
EmitSmallSigma(a, r12d, ecx, r8d, 7, 18, 3);
a.add(r12d, edx);
a.mov(Dword(Source1, 3), r9d);
a.mov(Dword(Source1, 2), r10d);
a.mov(Dword(Source1, 1), r11d);
a.mov(Dword(Source1, 0), r12d);
}
private static void EmitSha256Msg2(Assembler a)
{
// w16 = s1[0] + sigma1(w14); w17 = s1[1] + sigma1(w15); w18 and w19 use w16 and w17.
a.mov(esi, Dword(Source2, 2));
EmitSmallSigma(a, eax, esi, r8d, 17, 19, 10);
a.add(eax, Dword(Source1, 0));
a.mov(esi, Dword(Source2, 3));
EmitSmallSigma(a, ebx, esi, r8d, 17, 19, 10);
a.add(ebx, Dword(Source1, 1));
EmitSmallSigma(a, ecx, eax, r8d, 17, 19, 10);
a.add(ecx, Dword(Source1, 2));
EmitSmallSigma(a, edx, ebx, r8d, 17, 19, 10);
a.add(edx, Dword(Source1, 3));
a.mov(Dword(Source1, 0), eax);
a.mov(Dword(Source1, 1), ebx);
a.mov(Dword(Source1, 2), ecx);
a.mov(Dword(Source1, 3), edx);
}
// result = ror(x, r1) ^ ror(x, r2) ^ ror(x, r3)
private static void EmitRotateXor(
Assembler a, AssemblerRegister32 result, AssemblerRegister32 x, AssemblerRegister32 temp, int r1, int r2, int r3)
{
a.mov(result, x);
a.ror(result, (byte)r1);
a.mov(temp, x);
a.ror(temp, (byte)r2);
a.xor(result, temp);
a.mov(temp, x);
a.ror(temp, (byte)r3);
a.xor(result, temp);
}
// result = ror(x, r1) ^ ror(x, r2) ^ (x >> shift)
private static void EmitSmallSigma(
Assembler a, AssemblerRegister32 result, AssemblerRegister32 x, AssemblerRegister32 temp, int r1, int r2, int shift)
{
a.mov(result, x);
a.ror(result, (byte)r1);
a.mov(temp, x);
a.ror(temp, (byte)r2);
a.xor(result, temp);
a.mov(temp, x);
a.shr(temp, (byte)shift);
a.xor(result, temp);
}
private static AssemblerRegisterXMM ToXmm(Register register) => new(register);
private static AssemblerRegister64 ToGpr64(Register register) => new(register.GetFullRegister());
}

View File

@ -10,8 +10,6 @@ public interface IVirtualMemory : ICpuMemory
{
void Clear();
bool IsBackedView(ulong address);
void Map(ulong virtualAddress, ulong memorySize, ulong fileOffset, ReadOnlySpan<byte> fileData, ProgramHeaderFlags protection);
IReadOnlyList<VirtualMemoryRegion> SnapshotRegions();

File diff suppressed because it is too large Load Diff

View File

@ -11,8 +11,6 @@ public sealed class VirtualMemory : IVirtualMemory
private readonly object _gate = new();
private readonly List<MappedRegion> _regions = new();
public bool IsBackedView(ulong address) => false;
public void Clear()
{
lock (_gate)

View File

@ -7,16 +7,12 @@ using SharpEmu.Core.Cpu.Disasm;
using SharpEmu.Core.Loader;
using SharpEmu.Core.Memory;
using SharpEmu.HLE;
using SharpEmu.HLE.Host;
using SharpEmu.HLE.GpuMemory;
using SharpEmu.Libs.VideoOut;
using SharpEmu.Libs.Kernel;
using SharpEmu.Libs.AppContent;
using SharpEmu.Libs.SaveData;
using SharpEmu.Libs.Fiber;
using SharpEmu.Libs.SystemService;
using SharpEmu.Libs.Network;
using SharpEmu.Libs.Np;
using System.Buffers.Binary;
using System.Collections.Generic;
using System.Linq;
@ -41,9 +37,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
private readonly ISymbolCatalog _symbolCatalog;
private readonly CpuExecutionOptions _cpuExecutionOptions;
private readonly IFileSystem _fileSystem;
private readonly GuestGpuMemory? _gpuMemory;
private readonly int _systemLanguage;
private readonly object _dynamicModuleGate = new();
private bool _disposed;
public string? LastExecutionDiagnostics { get; private set; }
@ -63,11 +56,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
IModuleManager moduleManager,
ISymbolCatalog? symbolCatalog = null,
CpuExecutionOptions cpuExecutionOptions = default,
IFileSystem? fileSystem = null,
GuestGpuMemory? gpuMemory = null,
int systemLanguage = 1)
IFileSystem? fileSystem = null)
{
_gpuMemory = gpuMemory;
_selfLoader = selfLoader ?? throw new ArgumentNullException(nameof(selfLoader));
_virtualMemory = virtualMemory ?? throw new ArgumentNullException(nameof(virtualMemory));
_cpuDispatcher = cpuDispatcher ?? throw new ArgumentNullException(nameof(cpuDispatcher));
@ -81,7 +71,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
DebugHook = cpuExecutionOptions.DebugHook,
};
_fileSystem = fileSystem ?? new PhysicalFileSystem();
_systemLanguage = systemLanguage;
}
public static ISharpEmuRuntime CreateDefault(SharpEmuRuntimeOptions options = default)
@ -93,17 +82,13 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
ImportTraceLimit = Math.Max(0, options.ImportTraceLimit),
DebugHook = options.DebugHook,
};
var virtualMemory = new PhysicalVirtualMemory(
viewHost: HostViewMemory.Create(),
preReserveGuestAddressSpace: true);
var moduleManager = new ModuleManager();
// The compile-time generated registry (SharpEmu.SourceGenerators) is the sole
// registration source; content tests in SharpEmu.Libs.Tests pin its invariants.
moduleManager.RegisterExports(SharpEmu.Generated.SysAbiExportRegistry.CreateExports(Generation.Gen4 | Generation.Gen5));
moduleManager.Freeze();
var gpuMemory = new GuestGpuMemory(virtualMemory);
GuestGpuMemoryHook.Attach(gpuMemory);
var virtualMemory = new PhysicalVirtualMemory();
var fileSystem = new PhysicalFileSystem();
@ -114,9 +99,7 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
moduleManager,
Aerolib.Instance,
cpuExecutionOptions,
fileSystem,
gpuMemory,
options.SystemLanguage ?? 1);
fileSystem);
}
public SelfImage LoadImage(string ebootPath)
@ -157,15 +140,12 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
LastBasicBlockTrace = null;
LastMilestoneLog = null;
FiberExports.ResetRuntimeState();
Http2Exports.ResetRuntimeState();
NpAuthExports.ResetRuntimeState();
NpUniversalDataSystemExports.ResetRuntimeState();
KernelModuleRegistry.Reset();
var image = LoadImage(normalizedEbootPath);
VideoOutExports.ConfigureApplicationInfo(image.Title, image.TitleId, image.Version);
KernelMemoryCompatExports.ConfigureApplicationInfo(image.TitleId);
SaveDataExports.ConfigureApplicationInfo(image.TitleId);
SystemServiceExports.ConfigureApplicationInfo(image.TitleId, _systemLanguage);
SystemServiceExports.ConfigureApplicationInfo(image.TitleId);
_ = RegisterLoadedModule(normalizedEbootPath, image, isMain: true, isSystemModule: false);
KernelRuntimeCompatExports.ConfigureProcessProcParamAddress(image.ProcParamAddress);
Console.Error.WriteLine($"[RUNTIME] Entry: 0x{image.EntryPoint:X16}");
@ -180,16 +160,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
HleDataSymbols.ConfigureProcessImageName(processImageName);
MergeKnownHleDataSymbols(activeRuntimeSymbols);
var loadedModuleImages = LoadAdjacentSceModules(ebootPath, image, activeImportStubs, activeRuntimeSymbols);
var loadedModuleImages = LoadAdjacentSceModules(ebootPath, activeImportStubs, activeRuntimeSymbols);
RebindImportedDataSymbols(image, loadedModuleImages, activeRuntimeSymbols);
var app0Root = Path.GetDirectoryName(normalizedEbootPath) ?? string.Empty;
KernelModuleRegistry.ConfigureModuleLoader(modulePath => LoadRequestedAppModule(
modulePath,
app0Root,
image,
loadedModuleImages,
activeImportStubs,
activeRuntimeSymbols));
var initializerResult = RunAllInitializers(
image,
loadedModuleImages,
@ -655,7 +627,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
private List<LoadedModuleImage> LoadAdjacentSceModules(
string ebootPath,
SelfImage mainImage,
IDictionary<ulong, string> importStubs,
IDictionary<string, ulong> runtimeSymbols)
{
@ -668,20 +639,16 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
var moduleDirectories = new[]
{
(Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
(Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
(Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
// A linked PRX can be beside eboot.bin. Load only the app-root modules
// named by the main image at boot, after the normal module directories
// they may depend on. Map the other app-root modules so
// sceKernelLoadStartModule can start them later.
(Path: ebootDirectory, StartAtBoot: true, LinkedOnly: true, SearchOption: SearchOption.TopDirectoryOnly),
(Path: ebootDirectory, StartAtBoot: false, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
(Path: Path.Combine(ebootDirectory, "sce_module"), StartAtBoot: true),
(Path: Path.Combine(ebootDirectory, "sce_modules"), StartAtBoot: true),
(Path: Path.Combine(ebootDirectory, "Media", "Modules"), StartAtBoot: true),
// Unity native plugins are loaded later through sceKernelLoadStartModule. Map
// them up front so the HLE loader can return a real module handle and dlsym
// can resolve their exports, but defer DT_INIT until the guest requests them.
(Path: Path.Combine(ebootDirectory, "Media", "Plugins"), StartAtBoot: false, LinkedOnly: false, SearchOption: SearchOption.TopDirectoryOnly),
(Path: Path.Combine(ebootDirectory, "Media", "Plugins"), StartAtBoot: false),
}
.GroupBy(entry => entry.Path, StringComparer.OrdinalIgnoreCase)
.Select(group => group.First())
.Where(entry => Directory.Exists(entry.Path))
.ToArray();
@ -692,9 +659,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
var allModulePaths = moduleDirectories
.SelectMany(directory => Directory
.EnumerateFiles(directory.Path, "*", directory.SearchOption)
.EnumerateFiles(directory.Path)
.OrderBy(path => path, StringComparer.OrdinalIgnoreCase)
.Where(path => !directory.LinkedOnly || IsMainImageLinkedModule(path, mainImage.ImportedModuleNames))
.Select(path => (Path: path, directory.StartAtBoot)))
.Where(entry =>
{
@ -781,176 +747,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return loadedImages;
}
private KernelModuleRegistry.ModuleLoadResult LoadRequestedAppModule(
string guestModulePath,
string app0Root,
SelfImage mainImage,
List<LoadedModuleImage> loadedModuleImages,
Dictionary<ulong, string> activeImportStubs,
Dictionary<string, ulong> activeRuntimeSymbols)
{
lock (_dynamicModuleGate)
{
if (!TryResolveApp0ModulePath(app0Root, guestModulePath, out var modulePath))
{
Console.Error.WriteLine(
$"[RUNTIME] Runtime module path rejected: '{guestModulePath}'");
return KernelModuleRegistry.ModuleLoadResult.Failure(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
if (KernelModuleRegistry.TryFindByExactPath(modulePath, out var existingModule))
{
return KernelModuleRegistry.ModuleLoadResult.Success(existingModule.Handle);
}
try
{
var fileInfo = new FileInfo(modulePath);
if (!fileInfo.Exists || fileInfo.Length <= 0 || fileInfo.Length > int.MaxValue)
{
return KernelModuleRegistry.ModuleLoadResult.Failure(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
var moduleBytes = GC.AllocateUninitializedArray<byte>((int)fileInfo.Length);
using (var stream = File.OpenRead(modulePath))
{
stream.ReadExactly(moduleBytes);
}
var moduleImage = _selfLoader.LoadAdditional(
moduleBytes.AsSpan(),
_virtualMemory,
_moduleManager,
_fileSystem,
Path.GetDirectoryName(modulePath));
var candidateImportStubs = new Dictionary<ulong, string>(activeImportStubs);
var candidateRuntimeSymbols = new Dictionary<string, ulong>(activeRuntimeSymbols, StringComparer.Ordinal);
_ = MergeImportStubs(candidateImportStubs, moduleImage.ImportStubs, modulePath);
_ = MergeRuntimeSymbols(candidateRuntimeSymbols, moduleImage.RuntimeSymbols);
InstallNativePluginCompatibilityHooks(candidateImportStubs, moduleImage, modulePath);
string? installError = null;
if (_cpuDispatcher is not CpuDispatcher dispatcher ||
!dispatcher.TryInstallAdditionalModule(
candidateImportStubs,
candidateRuntimeSymbols,
out installError))
{
Console.Error.WriteLine(
$"[RUNTIME] Runtime module import installation failed: {modulePath} " +
$"({installError ?? "unsupported CPU backend"})");
return KernelModuleRegistry.ModuleLoadResult.Failure(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_IMPLEMENTED);
}
ReplaceDictionary(activeImportStubs, candidateImportStubs);
ReplaceDictionary(activeRuntimeSymbols, candidateRuntimeSymbols);
var handle = RegisterLoadedModule(
modulePath,
moduleImage,
isMain: false,
isSystemModule: false);
loadedModuleImages.Add(new LoadedModuleImage(modulePath, moduleImage, handle, StartAtBoot: false));
RebindImportedDataSymbols(mainImage, loadedModuleImages, activeRuntimeSymbols);
Console.Error.WriteLine(
$"[RUNTIME] Loaded requested module '{guestModulePath}' as {Path.GetFileName(modulePath)}: " +
$"handle={handle}, imports={moduleImage.ImportStubs.Count}, symbols={moduleImage.RuntimeSymbols.Count}");
return KernelModuleRegistry.ModuleLoadResult.Success(handle);
}
catch (Exception ex)
{
Console.Error.WriteLine(
$"[RUNTIME] Runtime module load failed: {modulePath} " +
$"({ex.GetType().Name}: {ex.Message})");
return KernelModuleRegistry.ModuleLoadResult.Failure(
(int)OrbisGen2Result.ORBIS_GEN2_ERROR_NOT_FOUND);
}
}
}
private static bool TryResolveApp0ModulePath(
string app0Root,
string guestModulePath,
out string modulePath)
{
modulePath = string.Empty;
if (string.IsNullOrWhiteSpace(app0Root) || string.IsNullOrWhiteSpace(guestModulePath))
{
return false;
}
var normalizedGuestPath = guestModulePath.Replace('\\', '/');
string relativePath;
if (normalizedGuestPath.StartsWith("/app0/", StringComparison.OrdinalIgnoreCase))
{
relativePath = normalizedGuestPath[6..];
}
else if (normalizedGuestPath.StartsWith("app0/", StringComparison.OrdinalIgnoreCase))
{
relativePath = normalizedGuestPath[5..];
}
else if (!normalizedGuestPath.StartsWith("/", StringComparison.Ordinal))
{
relativePath = normalizedGuestPath;
}
else
{
return false;
}
var root = Path.GetFullPath(app0Root);
var candidate = Path.GetFullPath(
Path.Combine(root, relativePath.Replace('/', Path.DirectorySeparatorChar)));
var rootPrefix = Path.TrimEndingDirectorySeparator(root) + Path.DirectorySeparatorChar;
if (!candidate.StartsWith(rootPrefix, HostPathComparison))
{
return false;
}
modulePath = candidate;
return true;
}
private static StringComparison HostPathComparison => OperatingSystem.IsWindows()
? StringComparison.OrdinalIgnoreCase
: StringComparison.Ordinal;
private static void ReplaceDictionary<TKey, TValue>(
IDictionary<TKey, TValue> destination,
IReadOnlyDictionary<TKey, TValue> source)
where TKey : notnull
{
destination.Clear();
foreach (var entry in source)
{
destination.Add(entry);
}
}
private static bool IsMainImageLinkedModule(string modulePath, IReadOnlyList<string> importedModuleNames)
{
var fileName = Path.GetFileName(modulePath);
var fileNameWithoutExtension = Path.GetFileNameWithoutExtension(modulePath);
if (string.IsNullOrWhiteSpace(fileName) || string.IsNullOrWhiteSpace(fileNameWithoutExtension))
{
return false;
}
for (var i = 0; i < importedModuleNames.Count; i++)
{
var importedName = importedModuleNames[i];
if (string.Equals(importedName, fileName, StringComparison.OrdinalIgnoreCase) ||
string.Equals(importedName, fileNameWithoutExtension, StringComparison.OrdinalIgnoreCase))
{
return true;
}
}
return false;
}
private static void InstallNativePluginCompatibilityHooks(
IDictionary<ulong, string> importStubs,
SelfImage moduleImage,
@ -1009,7 +805,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
Environment.GetEnvironmentVariable("SHARPEMU_LOG_DATA_REBIND"),
"1",
StringComparison.Ordinal);
Span<byte> relocationValueBytes = stackalloc byte[sizeof(ulong)];
for (var i = 0; i < image.ImportedRelocations.Count; i++)
{
var relocation = image.ImportedRelocations[i];
@ -1036,19 +831,8 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
{
if (logRebind)
{
var readable = _virtualMemory.TryRead(relocation.TargetAddress, relocationValueBytes);
var currentValue = readable ? BinaryPrimitives.ReadUInt64LittleEndian(relocationValueBytes) : 0;
Console.Error.WriteLine(
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16} readable={readable} current=0x{currentValue:X16}");
foreach (var region in _virtualMemory.SnapshotRegions())
{
if (relocation.TargetAddress >= region.VirtualAddress &&
relocation.TargetAddress - region.VirtualAddress < region.MemorySize)
{
Console.Error.WriteLine(
$"[RUNTIME] Imported data target-region: start=0x{region.VirtualAddress:X16} size=0x{region.MemorySize:X} protection={region.Protection}");
}
}
$"[RUNTIME] Imported data write-failed: nid={relocation.Nid} target=0x{relocation.TargetAddress:X16} value=0x{reboundValue:X16}");
}
unresolved++;
@ -1378,7 +1162,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
}
_disposed = true;
KernelModuleRegistry.ConfigureModuleLoader(null);
if (_cpuDispatcher is IDisposable disposableDispatcher)
{
@ -1395,17 +1178,6 @@ public sealed class SharpEmuRuntime : ISharpEmuRuntime
return;
}
if (_gpuMemory != null)
{
if (GuestGpuMemoryHook.TryTakeShutdownSummary(out var gpuSummary))
{
Console.Error.WriteLine("[LOADER][INFO] " + gpuSummary);
}
GuestGpuMemoryHook.Attach(null);
_gpuMemory.Dispose();
}
KernelMemoryCompatExports.ResetBackingMappings(_virtualMemory as IGuestBackedSpace);
if (_virtualMemory is IDisposable disposableMemory)
{
disposableMemory.Dispose();

View File

@ -10,8 +10,6 @@ public readonly struct SharpEmuRuntimeOptions
{
public CpuExecutionEngine CpuEngine { get; init; }
public int? SystemLanguage { get; init; }
public bool StrictDynlibResolution { get; init; }
public int ImportTraceLimit { get; init; }

View File

@ -22,22 +22,18 @@ public sealed class ConsoleWindow : Window
private readonly ListBox _list;
private readonly TextBox _searchBox;
private readonly CheckBox _autoScrollCheck;
private readonly GuiSettings _settings;
private bool _placementRestored;
public ConsoleWindow(
AvaloniaList<LogLine> lines,
Action clear,
bool autoScroll,
GuiSettings settings)
bool autoScroll)
{
var loc = Localization.Instance;
_sourceLines = lines;
_settings = settings;
Title = loc.Get("Console.WindowTitle");
Width = settings.ConsoleWindowWidth;
Height = settings.ConsoleWindowHeight;
Width = 980;
Height = 620;
MinWidth = 520;
MinHeight = 320;
Background = new SolidColorBrush(Color.Parse("#0D1017"));
@ -119,66 +115,9 @@ public sealed class ConsoleWindow : Window
lines.CollectionChanged += OnLinesChanged;
Closed += (_, _) => lines.CollectionChanged -= OnLinesChanged;
Opened += (_, _) => RestorePlacement();
PositionChanged += (_, _) => RememberNormalBounds();
SizeChanged += (_, _) => RememberNormalBounds();
PropertyChanged += (_, change) =>
{
if (_placementRestored && change.Property == WindowStateProperty && WindowState != WindowState.Minimized)
_settings.ConsoleWindowMaximized = WindowState == WindowState.Maximized;
};
Closing += (_, _) =>
{
RememberNormalBounds();
if (WindowState != WindowState.Minimized)
_settings.ConsoleWindowMaximized = WindowState == WindowState.Maximized;
_settings.Save();
};
RefreshVisibleLines();
}
private void RestorePlacement()
{
if (_settings.ConsoleWindowLeft is { } left && _settings.ConsoleWindowTop is { } top)
{
var savedPosition = new PixelPoint(left, top);
var screen = Screens.ScreenFromPoint(savedPosition) ?? Screens.ScreenFromWindow(Owner ?? this) ?? Screens.Primary;
if (screen is not null)
{
Width = Math.Min(Width, Math.Max(MinWidth, screen.WorkingArea.Width / screen.Scaling));
Height = Math.Min(Height, Math.Max(MinHeight, screen.WorkingArea.Height / screen.Scaling));
Position = ConstrainPosition(savedPosition, screen.WorkingArea, new Size(Width, Height), screen.Scaling);
}
}
_placementRestored = true;
RememberNormalBounds();
if (_settings.ConsoleWindowMaximized)
WindowState = WindowState.Maximized;
}
private void RememberNormalBounds()
{
if (!_placementRestored || !IsVisible || WindowState != WindowState.Normal)
return;
_settings.ConsoleWindowLeft = Position.X;
_settings.ConsoleWindowTop = Position.Y;
if (ClientSize.Width >= MinWidth && ClientSize.Height >= MinHeight)
{
_settings.ConsoleWindowWidth = ClientSize.Width;
_settings.ConsoleWindowHeight = ClientSize.Height;
}
}
internal static PixelPoint ConstrainPosition(PixelPoint position, PixelRect workingArea, Size size, double scaling)
{
var maximumLeft = workingArea.Right - (int)Math.Ceiling(size.Width * scaling);
var maximumTop = workingArea.Bottom - (int)Math.Ceiling(size.Height * scaling);
return new PixelPoint(
Math.Clamp(position.X, workingArea.X, Math.Max(workingArea.X, maximumLeft)),
Math.Clamp(position.Y, workingArea.Y, Math.Max(workingArea.Y, maximumTop)));
}
private void OnLinesChanged(object? sender, NotifyCollectionChangedEventArgs e)
{
RefreshVisibleLines();

View File

@ -1,76 +0,0 @@
<!--
Copyright (C) 2026 SharpEmu Emulator Project
SPDX-License-Identifier: GPL-2.0-or-later
-->
<Window xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:SharpEmu.GUI"
x:Class="SharpEmu.GUI.CustomEnvironmentDialog"
Width="620"
Height="520"
MinWidth="520"
MinHeight="430"
CanResize="True"
WindowStartupLocation="CenterOwner"
Background="{StaticResource BgBrush}"
Title="{Binding [Launch.CustomEnv.Title], Source={x:Static local:Localization.Instance}}">
<Grid Margin="24" RowDefinitions="Auto,Auto,Auto,*,Auto,Auto">
<TextBlock Grid.Row="0"
Text="{Binding [Launch.CustomEnv.Title], Source={x:Static local:Localization.Instance}}"
FontSize="24"
FontWeight="Bold" />
<TextBlock Grid.Row="1"
Margin="0,8,0,16"
Text="{Binding [Launch.CustomEnv.Desc], Source={x:Static local:Localization.Instance}}"
Foreground="{StaticResource MutedBrush}"
TextWrapping="Wrap" />
<Button x:Name="DocumentationButton"
Grid.Row="2"
Classes="ghost"
HorizontalAlignment="Left"
Margin="0,0,0,12"
Padding="0,4"
Content="{Binding [Launch.CustomEnv.Documentation], Source={x:Static local:Localization.Instance}}" />
<TextBox x:Name="EnvironmentTextBox"
Grid.Row="3"
AcceptsReturn="True"
TextWrapping="NoWrap"
FontFamily="Consolas,monospace"
FontSize="13"
VerticalContentAlignment="Top"
PlaceholderText="{Binding [Launch.CustomEnv.Placeholder], Source={x:Static local:Localization.Instance}}" />
<TextBlock x:Name="StatusText"
Grid.Row="4"
Margin="0,12,0,0"
Foreground="{StaticResource MutedBrush}"
TextWrapping="Wrap" />
<Grid Grid.Row="5"
Margin="0,18,0,0"
ColumnDefinitions="Auto,Auto,*,Auto,Auto">
<Button x:Name="SaveGameButton"
Grid.Column="0"
Classes="ghost"
Margin="0,0,8,0"
Content="{Binding [Launch.CustomEnv.SaveGame], Source={x:Static local:Localization.Instance}}" />
<Button x:Name="SaveGlobalButton"
Grid.Column="1"
Classes="ghost"
Content="{Binding [Launch.CustomEnv.SaveGlobal], Source={x:Static local:Localization.Instance}}" />
<Button x:Name="CancelButton"
Grid.Column="3"
Classes="ghost"
Margin="0,0,8,0"
Content="{Binding [Common.Cancel], Source={x:Static local:Localization.Instance}}" />
<Button x:Name="PlayButton"
Grid.Column="4"
Classes="accent"
Content="{Binding [Launch.CustomEnv.Play], Source={x:Static local:Localization.Instance}}" />
</Grid>
</Grid>
</Window>

View File

@ -1,92 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using Avalonia.Controls;
using System.Diagnostics;
namespace SharpEmu.GUI;
public partial class CustomEnvironmentDialog : Window
{
private readonly string? _titleId;
public CustomEnvironmentDialog()
: this(null, [])
{
}
public CustomEnvironmentDialog(string? titleId, IEnumerable<string> initialEntries)
{
InitializeComponent();
_titleId = titleId;
EnvironmentTextBox.Text = string.Join(Environment.NewLine, initialEntries);
SaveGameButton.IsEnabled = !string.IsNullOrWhiteSpace(titleId);
SaveGameButton.Click += (_, _) => SaveForGame();
SaveGlobalButton.Click += (_, _) => SaveGlobally();
DocumentationButton.Click += (_, _) => OpenDocumentation();
CancelButton.Click += (_, _) => Close(null);
PlayButton.Click += (_, _) => Play();
}
public event Action<IReadOnlyList<string>>? SaveGlobalRequested;
public event Action<string, IReadOnlyList<string>>? SaveGameRequested;
private static void OpenDocumentation()
{
Process.Start(new ProcessStartInfo
{
FileName = ProjectLinks.DocumentUrl("docs/sharpemu-gui-undocumented-env-vars.md"),
UseShellExecute = true,
});
}
private bool TryGetEntries(out IReadOnlyList<string> entries)
{
if (!CustomEnvironmentVariables.TryParseLines(
EnvironmentTextBox.Text,
out var parsed,
out var invalidLine))
{
StatusText.Text = string.Format(
Localization.Instance.Get("Launch.CustomEnv.Invalid"),
invalidLine);
entries = [];
return false;
}
StatusText.Text = string.Empty;
entries = parsed;
return true;
}
private void SaveForGame()
{
if (string.IsNullOrWhiteSpace(_titleId) || !TryGetEntries(out var entries))
{
return;
}
SaveGameRequested?.Invoke(_titleId, entries);
StatusText.Text = Localization.Instance.Get("Launch.CustomEnv.SavedGame");
}
private void SaveGlobally()
{
if (!TryGetEntries(out var entries))
{
return;
}
SaveGlobalRequested?.Invoke(entries);
StatusText.Text = Localization.Instance.Get("Launch.CustomEnv.SavedGlobal");
}
private void Play()
{
if (TryGetEntries(out var entries))
{
Close(entries);
}
}
}

View File

@ -1,70 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.GUI;
internal static class CustomEnvironmentVariables
{
public static bool TryParseLines(
string? text,
out IReadOnlyList<string> entries,
out string invalidLine)
{
var parsed = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var rawLine in (text ?? string.Empty).Split(['\r', '\n'], StringSplitOptions.RemoveEmptyEntries))
{
var line = rawLine.Trim();
if (line.Length == 0 || line.StartsWith('#'))
{
continue;
}
if (!TryParseEntry(line, out var name, out var value))
{
entries = [];
invalidLine = line;
return false;
}
parsed[name] = value;
}
entries = parsed.Select(pair => $"{pair.Key}={pair.Value}").ToArray();
invalidLine = string.Empty;
return true;
}
public static IReadOnlyList<string> Merge(
params IEnumerable<string>?[] sources)
{
var merged = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var source in sources)
{
if (source is null)
{
continue;
}
foreach (var entry in source)
{
if (TryParseEntry(entry, out var name, out var value))
{
merged[name] = value;
}
}
}
return merged.Select(pair => $"{pair.Key}={pair.Value}").ToArray();
}
public static bool TryParseEntry(string entry, out string name, out string value)
{
var separator = entry.IndexOf('=');
name = (separator >= 0 ? entry[..separator] : entry).Trim();
value = separator >= 0 ? entry[(separator + 1)..] : "1";
return name.Length != 0 &&
!name.Contains('\0') &&
!name.Contains('=');
}
}

View File

@ -1,58 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Text.Json;
namespace SharpEmu.GUI;
internal sealed class GlobalCustomEnvironmentSettings
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
WriteIndented = true,
};
public List<string> EnvironmentVariables { get; set; } = new();
public static string FilePath =>
Path.Combine(AppContext.BaseDirectory, "user", "custom_envs.json");
public static IReadOnlyList<string> Load()
{
try
{
if (!File.Exists(FilePath))
{
return [];
}
var settings = JsonSerializer.Deserialize<GlobalCustomEnvironmentSettings>(
File.ReadAllText(FilePath),
SerializerOptions);
return Normalize(settings?.EnvironmentVariables);
}
catch (Exception)
{
return [];
}
}
public static void Save(IEnumerable<string> entries)
{
try
{
Directory.CreateDirectory(Path.GetDirectoryName(FilePath)!);
var settings = new GlobalCustomEnvironmentSettings
{
EnvironmentVariables = Normalize(entries).ToList(),
};
File.WriteAllText(FilePath, JsonSerializer.Serialize(settings, SerializerOptions));
}
catch (Exception)
{
}
}
private static IReadOnlyList<string> Normalize(IEnumerable<string>? entries) =>
CustomEnvironmentVariables.Merge(entries);
}

View File

@ -42,22 +42,11 @@ public sealed class GuiSettings
public string LibraryLayout { get; set; } = "Carousel";
public double EmbeddedConsoleHeight { get; set; } = 240;
public double ConsoleWindowWidth { get; set; } = 980;
public double ConsoleWindowHeight { get; set; } = 620;
public int? ConsoleWindowLeft { get; set; }
public int? ConsoleWindowTop { get; set; }
public bool ConsoleWindowMaximized { get; set; }
public string? EmulatorPath { get; set; }
/// <summary>UI language, matching a file code under Languages/ (e.g. "en", "tr").</summary>
public string Language { get; set; } = "en";
public string ConsoleType { get; set; } = "PS5";
public string ConsoleLanguage { get; set; } = "EnglishUS";
/// <summary>Default text-entry profile exposed to games.</summary>
public string DefaultProfile { get; set; } = "Sharp";
@ -80,14 +69,8 @@ public sealed class GuiSettings
public string HdrMode { get; set; } = "Auto";
public bool OverlayEnabled { get; set; } = true;
public string OverlayCorner { get; set; } = "TopRight";
public string OverlayMode { get; set; } = "TitleBar";
/// <summary>Names of SHARPEMU_* switches set to "1" in the emulator's environment at launch.</summary>
public List<string> EnvironmentToggles { get; set; } = ["SHARPEMU_WRITABLE_APP0", "SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"];
public string BinkPlaybackMode { get; set; } = "Guest";
public List<string> EnvironmentToggles { get; set; } = new();
/// <summary>Internal render resolution scale (1.0 = native, 0.5 = half).</summary>
public double RenderResolutionScale { get; set; } = 1.0;
@ -131,7 +114,7 @@ public sealed class GuiSettings
settings.GameFolders = FilterNullOrEmpty(settings.GameFolders);
settings.ExcludedGames = FilterNullOrEmpty(settings.ExcludedGames);
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles ?? ["SHARPEMU_WRITABLE_APP0", "SHARPEMU_DISABLE_IMPORT_LOOP_GUARD"]);
settings.EnvironmentToggles = FilterNullOrEmpty(settings.EnvironmentToggles);
settings.LogLevel ??= "Info";
settings.Language ??= "en";
var legacyProfile = settings.EnvironmentToggles
@ -147,58 +130,17 @@ public sealed class GuiSettings
settings.DefaultProfile = NormalizeDefaultProfile(
legacyProfile is { Length: 2 } ? legacyProfile[1] : settings.DefaultProfile);
settings.DiscordClientId ??= "1525606762248540221";
if (settings.RenderResolutionScale <= 0 || settings.RenderResolutionScale > 2.0)
{
settings.RenderResolutionScale = 1.0;
}
settings.LibraryLayout = NormalizeChoice(settings.LibraryLayout, "Carousel", "Grid");
settings.ConsoleType = NormalizeChoice(settings.ConsoleType, "PS5");
settings.ConsoleLanguage = NormalizeChoice(
settings.ConsoleLanguage,
"EnglishUS",
"Japanese",
"French",
"SpanishSpain",
"German",
"Italian",
"Dutch",
"PortuguesePortugal",
"Russian",
"Korean",
"ChineseTraditional",
"ChineseSimplified",
"Finnish",
"Swedish",
"Danish",
"Norwegian",
"Polish",
"PortugueseBrazil",
"EnglishUK",
"Turkish",
"SpanishLatinAmerica",
"Arabic",
"FrenchCanada",
"Czech",
"Hungarian",
"Greek",
"Romanian",
"Thai",
"Vietnamese",
"Indonesian",
"Ukrainian");
settings.WindowMode = NormalizeChoice(settings.WindowMode, "Windowed", "Borderless", "Exclusive");
settings.Resolution = NormalizeResolution(settings.Resolution);
settings.ScalingMode = NormalizeChoice(settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
settings.HdrMode = NormalizeChoice(settings.HdrMode, "Auto", "On", "Off");
settings.BinkPlaybackMode = NormalizeChoice(settings.BinkPlaybackMode, "Guest", "Host", "Skip");
settings.OverlayCorner = NormalizeChoice(settings.OverlayCorner, "TopRight", "TopLeft", "BottomRight", "BottomLeft");
settings.OverlayMode = NormalizeChoice(settings.OverlayMode, "TitleBar", "Full", "Minimal");
settings.DisplayIndex = Math.Max(0, settings.DisplayIndex);
settings.RefreshRate = Math.Clamp(settings.RefreshRate, 0, 1000);
settings.EmbeddedConsoleHeight = NormalizeConsoleSize(settings.EmbeddedConsoleHeight, 240, 120);
settings.ConsoleWindowWidth = NormalizeConsoleSize(settings.ConsoleWindowWidth, 980, 520);
settings.ConsoleWindowHeight = NormalizeConsoleSize(settings.ConsoleWindowHeight, 620, 320);
if (!settings.ConsoleWindowLeft.HasValue || !settings.ConsoleWindowTop.HasValue)
{
settings.ConsoleWindowLeft = null;
settings.ConsoleWindowTop = null;
}
return settings;
}
@ -218,9 +160,6 @@ public sealed class GuiSettings
choices.Prepend(fallback).FirstOrDefault(
choice => string.Equals(choice, value, StringComparison.OrdinalIgnoreCase)) ?? fallback;
private static double NormalizeConsoleSize(double value, double fallback, double minimum) =>
double.IsFinite(value) && value >= minimum ? Math.Min(value, 16384) : fallback;
private static string NormalizeResolution(string? value)
{
if (!HostDisplayOptions.TryParseResolution(value, out var width, out var height))

View File

@ -1,21 +1,5 @@
{
"_languageName": "العربية",
"Options.Env.StrictCompute.Desc": "التوقف عند رفض مظللات الحوسبة والرسم (افتراضي).\nأوقف الخيار لتخطي العمل المتأثر. قد تظهر صور مفقودة أو أخطاء أو توقفات. أعد تشغيل اللعبة بعد التغيير.",
"Options.Env.PerformanceProfile.Desc": "سجّل أزمنة الأداء ومعدل FPS من التشغيل التالي للعبة.\nقد يقلل هذا من الأداء.",
"Options.Env.PerformanceFrameTrace.Desc": "احتفظ بآخر 4,096 فاصل عرض واكتبها في السجل عند الإغلاق.\nيتطلب تسجيل الأداء. أغلق اللعبة بشكل طبيعي لحفظ التتبع.",
"Options.OverlayEnabled.Label": "عرض الأداء",
"Options.OverlayEnabled.Desc": "عرض إحصاءات الأداء. يبدّل F1 العرض لهذه الجلسة فقط. يبدأ العرض مخفيًا عند ضبط SHARPEMU_OVERLAY=0.",
"Options.OverlayMode.Label": "نمط العرض",
"Options.OverlayMode.Desc": "يعرض الكامل جميع العدادات. يعرض المختصر وشريط العنوان FPS واستخدام CPU وGPU والوقت المنقضي. يُقاس GPU لكل عملية في Windows؛ وإلا يظهر N/A.",
"Options.OverlayCorner.Label": "زاوية الشاشة",
"Options.OverlayCorner.Desc": "اختر الزاوية المحفوظة. يغيّر F2 الزاوية لهذه الجلسة فقط. لا ينقل إحصاءات شريط العنوان.",
"Options.OverlayMode.Full": "كامل",
"Options.OverlayMode.Minimal": "مختصر",
"Options.OverlayMode.TitleBar": "شريط العنوان (مختصر)",
"Options.OverlayCorner.TopLeft": "أعلى اليسار",
"Options.OverlayCorner.TopRight": "أعلى اليمين",
"Options.OverlayCorner.BottomRight": "أسفل اليمين",
"Options.OverlayCorner.BottomLeft": "أسفل اليسار",
"Page.Library": "المكتبة",
"Page.Options": "الخيارات",
@ -69,8 +53,6 @@
"Options.TraceImports.Label": "حد تتبع الاستيراد",
"Options.TraceImports.Desc": "تتبع أول N استيراد لكل وحدة (0 = إيقاف).",
"Options.CrashDump.Label": "تفريغات الأعطال",
"Options.CrashDump.Desc": "احفظ تفريغات الأعطال بجانب سجل التشغيل (Windows x64 فقط). قد تكون التفريغات كبيرة وتحتوي على بيانات خاصة.",
"Options.LogToFile.Label": "التسجيل في ملف",
"Options.LogToFile.Desc": "نسخ مخرجات المحاكي إلى ملف سجل.",
@ -108,17 +90,6 @@
"Launch.Idle": "خامل",
"Launch.Console": "≡ نافذة السجلات",
"Launch.Launch": "▶ تشغيل",
"Launch.CustomEnv": "تشغيل بمتغيرات بيئة مخصصة",
"Launch.CustomEnv.Title": "تشغيل بمتغيرات بيئة مخصصة",
"Launch.CustomEnv.Desc": "أدخل متغير بيئة واحدًا في كل سطر بصيغة NAME=VALUE. القيم المخصصة تتجاوز إعدادات الواجهة وإعدادات اللعبة التي تحمل الاسم نفسه.",
"Launch.CustomEnv.Documentation": "عرض متغيرات البيئة غير الموثقة ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_EXAMPLE=1",
"Launch.CustomEnv.SaveGame": "حفظ للعبة",
"Launch.CustomEnv.SaveGlobal": "حفظ بشكل عام",
"Launch.CustomEnv.Play": "تشغيل",
"Launch.CustomEnv.Invalid": "متغير بيئة غير صالح: {0}",
"Launch.CustomEnv.SavedGame": "تم الحفظ لهذه اللعبة.",
"Launch.CustomEnv.SavedGlobal": "تم الحفظ بشكل عام.",
"Launch.Stop": "■ إيقاف",
"Launch.Running": "قيد التشغيل — {0}",
"Launch.Stopping": "جارٍ الإيقاف...",
@ -176,8 +147,9 @@
"Options.Env.LogNp.Desc": "تسجيل نداءات مكتبة NP (شبكة PlayStation) في وحدة التحكم.",
"Options.Env.Group.Debug": "تصحيح الأخطاء",
"Options.Env.Group.General": "عام",
"Options.Env.RenderDoc.Desc": "يحمّل واجهة RenderDoc داخل التطبيق حتى يمكن التقاط الإطارات من داخل المحاكي.\nاضغط F12 أثناء تشغيل اللعبة لالتقاط إطار واحد؛ تُحفظ اللقطات في user/logs/capture_logs/<TITLE_ID>.\nيتطلب تثبيت RenderDoc. يبطئ وحدة معالجة الرسوميات ويسبب تعليق بعض الألعاب، لذا اتركه معطلاً ما لم تكن تصحح الأخطاء.",
"Options.Env.DisableVkImplicitLayers.Desc": "عند تفعيل RenderDoc، قد تتسبب التراكبات الأخرى المتعارضة في فقدان الجهاز. قد يؤدي تفعيل هذا الخيار إلى حل المشكلة.",
"Options.Env.RenderDoc.Desc": "يحمّل واجهة RenderDoc داخل التطبيق حتى يمكن التقاط الإطارات من داخل المحاكي.\nاضغط F10 أثناء تشغيل اللعبة لالتقاط إطار واحد؛ تُحفظ اللقطات في user/logs/capture_logs/<TITLE_ID>.\nيتطلب تثبيت RenderDoc. يبطئ وحدة معالجة الرسوميات ويسبب تعليق بعض الألعاب، لذا اتركه معطلاً ما لم تكن تصحح الأخطاء.",
"Options.Env.GuestImageCpuSync.Desc": "إعادة رفع أسطح الضيف التي تعيد كتابتها شيفرة المعالج الخاصة باللعبة.\nاتركه مغلقًا عادة. شغّله للألعاب التي لا تصل أسطحها المرسومة بالمعالج إلى الشاشة.\nيكلّف أداءً ويسبب مشاكل في بعض الألعاب مثل GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "تسليم مقدّمات المخازن المؤقتة لأوامر GPU حتى عندما لا تلتقطها قائمة الانتظار المستهدفة أبدًا.\nاتركه مغلقًا عادة. شغّله للألعاب التي تتجمد أثناء انتظار حاجز GPU لا يُشار إليه أبدًا.",
"Common.Save": "حفظ",
"Common.Cancel": "إلغاء",
"Options.About": "حول",
@ -188,7 +160,7 @@
"About.Discord.Label": "دسكورد",
"About.Discord.Desc": "انضم إلى المجتمع واحصل على الدعم وتابع التطوير.",
"About.GithubButton": "ساهم على GitHub!",
"About.DiscordButton": "انضم إلى ديسكورد الخاص بنا!",
"About.DiscordComingSoon": "قريبًا",
"Updater.Auto.Label": "التحقق من التحديثات عند بدء التشغيل",
"Updater.Auto.Desc": "يتحقق من GitHub دون تأخير بدء التشغيل.",
"Updater.Label": "التحديثات",
@ -205,13 +177,11 @@
"Updater.Status.ChecksumFailed": "فشل التحديث المنزَّل في اجتياز تحقق SHA-256.",
"Updater.Status.Unsupported": "يتطلب التحديث التلقائي إصدار x64 لنظام Windows أو Linux أو macOS.",
"Options.Graphics": "الرسومات",
"Options.Console.Type.Label": "وحدة التحكم",
"Options.Console.Type.Desc": "طراز وحدة التحكم الذي يتم إبلاغ الألعاب به.",
"Options.Console.Language.Label": "لغة وحدة التحكم",
"Options.Console.Language.Desc": "لغة النظام التي يتم إبلاغ الألعاب بها.",
"Options.Section.Rendering": "التصيير",
"Options.Section.Display": "العرض",
"Options.RenderResolution.Label": "الدقة الداخلية",
"Options.RenderResolution.Desc": "تصيير الأهداف خارج الشاشة بدقة أقل من الدقة الأصلية ثم رفع دقتها عند العرض. تمنح القيم الأقل مساحة أكبر لوحدة معالجة الرسوميات مقابل جودة الصورة؛ يُطبق عند التشغيل التالي.",
"Options.RenderResolution.Native": "100% (أصلية)",
"Options.WindowMode.Label": "وضع النافذة",
"Options.WindowMode.Desc": "نافذة عادية، أو سطح مكتب بلا حدود، أو ملء شاشة حصري.",
"Options.WindowMode.Windowed": "نافذة",
@ -234,10 +204,5 @@
"Options.VSync.Desc": "استخدام عرض FIFO لإخراج خالٍ من تمزق الصورة.",
"Options.Hdr.Label": "إخراج HDR",
"Options.Hdr.Desc": "استخدام HDR عندما تدعمه الشاشة المحددة وواجهة الرسومات. يعود الوضع التلقائي إلى SDR.",
"Options.Hdr.Auto": "تلقائي",
"Options.Env.BinkPlayback.Label": "تشغيل فيديو Bink",
"Options.Env.BinkPlayback.Desc": "يقوم Host بتشغيل فيديوهات Bink عبر جسر FFmpeg، لكن قد تظهر مشكلات في بعض الألعاب.\nيستخدم Guest وحدة فك الترميز الخاصة بمحرك اللعبة.\nيتخطى Skip فيديوهات Bink، مما قد يجعل التطوير أسهل.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "تخطي"
"Options.Hdr.Auto": "تلقائي"
}

View File

@ -1,243 +0,0 @@
{
"_languageName": "Azərbaycanca",
"Page.Library": "Kitabxana",
"Page.Options": "Parametrlər",
"Page.GameCount.One": "1 oyun",
"Page.GameCount.Other": "{0} oyun",
"Library.SearchWatermark": "Kitabxanada axtar…",
"Library.AddFolder": "Qovluq əlavə et",
"Library.OpenFile": "Fayl aç…",
"Library.View.Grid": "Üst-üstə görünüş",
"Library.View.Carousel": "Sıra görünüşü",
"Library.Context.Launch": "İşə sal",
"Library.Context.OpenFolder": "Oyunun qovluğunu aç",
"Library.Context.CopyPath": "Yolu kopyala",
"Library.Context.CopyTitleId": "Oyunun ID-sini kopyala",
"Library.Context.GameSettings": "Oyunun parametrləri…",
"Library.Context.Remove": "Kitabxanadan çıxar",
"Library.Empty.Title": "Kitabxananız boşdur",
"Library.Empty.Hint": "Başlamaq üçün oyunlarınızın olduğu qovluğu əlavə edin.",
"Library.Empty.SearchTitle": "Axtarışınıza uyğun oyun tapılmadı",
"Library.Empty.SearchHint": "Kitabxanada “{0}” axtarışına uyğun heç nə yoxdur.",
"Library.Empty.AddFolder": "+ Oyun qovluğu əlavə et",
"Library.Loading": "Kitabxana yüklənir…",
"Library.Stat.Version": "Versiya",
"Library.Stat.Installed": "Quraşdırılıb",
"Library.Stat.TitleId": "Oyunun ID-si",
"Common.Back": "Geri qayıt",
"Options.General": "Ümumi",
"Options.Logging": "Jurnala yazma",
"Options.Env.Tab": "Mühit",
"Options.Section.Environment": "Mühit dəyişənləri",
"Options.Env.Desc": "İşə salınarkən emulyatora mühit dəyişənləri kimi ötürülən parametrlər.",
"Options.Env.Bthid.Desc": "Sükan/FFB üçün aralıq proqram təminatı fasiləsiz sorğu göndərən oyunlarda Bluetooth HID əlçatmaz kimi bildirilsin.\nAdətən söndürülmüş saxlayın. İlkin hazırlıq uğursuz olduqda bəzi oyunlar donur.",
"Options.Env.LoopGuard.Desc": "Eyni çağırışı həddindən artıq uzun müddət təkrarlayan oyunlar məcburi bağlanmasın.\nOyun yüklənərkən öz-özünə bağlanırsa, bunu sınayın.",
"Options.Env.WritableApp0.Desc": "Oyunlara quraşdırıldıqları qovluqda fayl yaratmağa və yazmağa icazə verilsin.\nYadda saxlanan oyun və ya konfiqurasiya məlumatlarını /app0 daxilinə yazan paketlənməmiş oyun nüsxələri üçün lazımdır.",
"Options.Env.VkValidation.Desc": "GPU xətalarını araşdırmaq üçün Vulkan yoxlama qatlarını aktivləşdirin.\nİşi ləngidir. Vulkan SDK quraşdırılmalıdır.",
"Options.Env.DumpSpirv.Desc": "AGC kölgələndiricilərini və onların SPIR-V formasına çevrilmiş nüsxələrini shader-dumps qovluğuna yazın.\nKölgələndirici və ya təsvirin yaradılması ilə bağlı xətaları bildirərkən istifadə edin.",
"Options.Env.LogDirectMemory.Desc": "Birbaşa yaddaş ayırmalarını və uğursuzluqları konsola yazın.\nOyun başlanarkən qəfil dayandıqda və ya bağlandıqda istifadə edin.",
"Options.Env.LogIo.Desc": "Faylların açılması, oxunması və yolların müəyyənləşdirilməsi əməliyyatlarını konsola yazın.\nOyun başlanarkən məlumat fayllarını tapa bilmədikdə istifadə edin.",
"Options.Env.LogNp.Desc": "NP (PlayStation Network) kitabxanasına edilən çağırışları konsola yazın.",
"Options.Env.Group.Debug": "Xətaların araşdırılması",
"Options.Env.Group.General": "Ümumi",
"Options.Env.RenderDoc.Desc": "Kadrları emulyatorun daxilindən çəkmək üçün RenderDoc tətbiqdaxili API-sini yükləyin.\nOyun işləyərkən bir kadr çəkmək üçün F10 düyməsini basın; çəkilən kadrlar user/logs/capture_logs/<TITLE_ID> qovluğunda saxlanılır.\nRenderDoc quraşdırılmalıdır. GPU-nu ləngidir və bəzi oyunların donmasına səbəb olur, buna görə xətaları araşdırmadıqda söndürülmüş saxlayın.",
"Options.Env.DisableVkImplicitLayers.Desc": "RenderDoc aktiv olduqda digər ziddiyyətli overlay-lər device loss-a səbəb ola bilər. Bu seçimi aktivləşdirmək problemi həll edə bilər.",
"Options.Env.GuestImageCpuSync.Desc": "Emulyasiya olunan sistemdə oyunun öz CPU kodunun yenidən yazdığı təsvir səthlərini təkrar yükləyin.\nAdətən söndürülmüş saxlayın. CPU ilə çəkilən səthləri ekranda görünməyən oyunlar üçün aktivləşdirin.\nPerformansı azaldır və GTA V kimi bəzi oyunların işini pisləşdirir.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "GPU əmr buferlərinin giriş əmrlərini hədəf növbəsi onları qəbul etmədikdə belə ötürün.\nAdətən söndürülmüş saxlayın. Heç vaxt siqnal verməyən GPU sinxronlaşdırma səddini gözləyərkən donan oyunlar üçün aktivləşdirin.",
"Options.DefaultProfile.Label": "Standart profil adı",
"Options.DefaultProfile.Desc": "Oyun mətn daxil edilməsini istədikdə istifadə olunan ad. Standart ad Sharp-dır.",
"Options.Section.Emulation": "Emulyasiya",
"Options.Section.Logging": "Jurnala yazma",
"Options.Section.Launcher": "Başladıcı",
"Options.Section.Rendering": "Təsvirin yaradılması",
"Options.Section.Display": "Ekran",
"Options.Graphics": "Qrafika",
"Options.Console.Type.Label": "Konsol",
"Options.Console.Type.Desc": "Oyunlara bildirilən konsol modeli.",
"Options.Console.Language.Label": "Konsol dili",
"Options.Console.Language.Desc": "Oyunlara bildirilən sistem dili.",
"Options.RenderResolution.Label": "Daxili təsvir ölçüsü",
"Options.RenderResolution.Desc": "Ekrandan kənar təsvir hədəflərini ilkin ölçüdən aşağı ölçüdə yaradıb göstərərkən böyüdün. Aşağı dəyərlər təsvir keyfiyyətini azaldaraq GPU yükünü yüngülləşdirir; növbəti işə salınmada qüvvəyə minir.",
"Options.RenderResolution.Native": "100% (ilkin)",
"Options.WindowMode.Label": "Pəncərə rejimi",
"Options.WindowMode.Desc": "Adi pəncərə, iş masasını tutan çərçivəsiz pəncərə və ya eksklüziv tam ekran.",
"Options.WindowMode.Windowed": "Pəncərəli",
"Options.WindowMode.Borderless": "Çərçivəsiz",
"Options.WindowMode.Exclusive": "Eksklüziv",
"Options.Resolution.Label": "Təsvir ölçüsü",
"Options.Resolution.Desc": "Pəncərənin başlanğıc ölçüsü və ya eksklüziv tam ekranın təsvir ölçüsü.",
"Options.Display.Label": "Ekran",
"Options.Display.Desc": "Pəncərənin mərkəzə yerləşdirilməsi və tam ekran üçün istifadə olunan monitor.",
"Options.RefreshRate.Label": "Yenilənmə tezliyi",
"Options.RefreshRate.Desc": "Eksklüziv tam ekranın yenilənmə tezliyi. Avtomatik seçim ən yaxın rejimi seçir.",
"Options.RefreshRate.Automatic": "Avtomatik",
"Options.Scaling.Label": "Miqyaslama",
"Options.Scaling.Desc": "Emulyasiya olunan sistemin ilkin təsvirini daxili ölçüsünü dəyişmədən miqyaslayın.",
"Options.Scaling.Fit": "Sığdır",
"Options.Scaling.Cover": "Tam ört",
"Options.Scaling.Stretch": "Dart",
"Options.Scaling.Integer": "Tam ədəd əmsalı ilə",
"Options.VSync.Label": "VSync",
"Options.VSync.Desc": "Təsvirin parçalanmadan göstərilməsi üçün FIFO təqdimatından istifadə edin.",
"Options.Hdr.Label": "HDR çıxışı",
"Options.Hdr.Desc": "Seçilmiş ekran və qrafik alt sistem dəstəklədikdə HDR istifadə edin. Avtomatik rejimdə dəstək yoxdursa, SDR istifadə olunur.",
"Options.Hdr.Auto": "Avtomatik",
"Options.Env.BinkPlayback.Label": "Bink video oynadılması",
"Options.Env.BinkPlayback.Desc": "Host Bink videolarını FFmpeg körpüsü vasitəsilə oynadır, lakin bəzi oyunlarda problemlər yarana bilər.\nGuest oyunun öz dekoderindən istifadə edir.\nSkip Bink videolarını ötürür və bu, inkişaf prosesini asanlaşdıra bilər.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Skip",
"Options.CpuEngine.Label": "CPU icra mühərriki",
"Options.CpuEngine.Desc": "Oyun kodunu işlətmək üçün istifadə olunan icra mühərriki.",
"Options.CpuEngine.Native": "Birbaşa icra",
"Options.Strict.Label": "Dinamik kitabxana simvollarının ciddi yoxlanması",
"Options.Strict.Desc": "İdxal olunan simvol tapılmadıqda işə salınmanı dayandırın.",
"Options.LogLevel.Label": "Jurnal səviyyəsi",
"Options.LogLevel.Desc": "Emulyatorun konsol çıxışının təfərrüat səviyyəsi.",
"Options.LogLevel.Trace": "İzləmə",
"Options.LogLevel.Debug": "Xətaların araşdırılması",
"Options.LogLevel.Info": "Məlumat",
"Options.LogLevel.Warning": "Xəbərdarlıq",
"Options.LogLevel.Error": "Xəta",
"Options.LogLevel.Critical": "Kritik",
"Options.TraceImports.Label": "İdxalların izlənmə həddi",
"Options.TraceImports.Desc": "Hər modulun ilk N idxalını izləyin (0 = sönülü).",
"Options.LogToFile.Label": "Jurnalı fayla yaz",
"Options.LogToFile.Desc": "Emulyatorun çıxışını jurnal faylına da yazın.",
"Options.LogFilePath.Label": "Jurnal faylının yolu",
"Options.LogFilePath.Default": "Fərdi yol yoxdur. Jurnallar emulyatorun yanındakı user/logs qovluğuna yazılır.",
"Options.LogFilePath.Select": "Seç…",
"Options.OverrideLogFile.Label": "Jurnal faylının yolunu olduğu kimi istifadə et",
"Options.OverrideLogFile.Desc": "Oyunun ID-sini və vaxt nişanını əlavə etmək əvəzinə fayl yolunu olduğu kimi istifadə edin.",
"Options.TitleMusic.Label": "Oyun musiqisi",
"Options.TitleMusic.Desc": "Kitabxanada seçilmiş oyunun önbaxış musiqisini təkrar səsləndirin.",
"Options.Discord.Label": "Discord vəziyyəti",
"Options.Discord.Desc": "İşləyən oyunu Discord profilinizdə göstərin.",
"Options.Language.Label": "Emulyatorun dili",
"Options.Language.Desc": "Başladıcı daxilində istifadə olunan dil. Dərhal tətbiq olunur.",
"Common.On": "Aktiv",
"Common.Off": "Deaktiv",
"Common.Save": "Yadda saxla",
"Common.Cancel": "Ləğv et",
"Console.Title": "Konsol",
"Console.SearchWatermark": "Axtar...",
"Console.AutoScroll": "Avtomatik sürüşdürmə",
"Console.Split": "Böl",
"Console.Copy": "Kopyala",
"Console.Clear": "Təmizlə",
"Console.WindowTitle": "SharpEmu konsolu",
"Launch.NoGameSelected": "Oyun seçilməyib",
"Launch.NoGameHint": "Kitabxanadan oyun seçin və ya birbaşa eboot.bin faylını açın.",
"Launch.Idle": "Gözləmədə",
"Launch.Console": "≡ Konsol",
"Launch.Launch": "▶ İşə sal",
"Launch.Stop": "■ Dayandır",
"Launch.Running": "İşləyir: {0}",
"Launch.Stopping": "Dayandırılır…",
"Launch.Exited": "{0} kodu ilə bağlandı ({1})",
"Launch.ExeNotFound": "SharpEmu icra faylı tapılmadı. Əvvəlcə SharpEmu.CLI layihəsini yığın (dotnet build).",
"Launch.LogFile": "Jurnal faylı: {0}",
"Launch.Command": "$ SharpEmu {0}",
"Launch.StartFailed": "Emulyatoru işə salmaq mümkün olmadı: {0}",
"Launch.ProcessExited": "Proses {0} kodu ilə bağlandı ({1}).",
"Exit.Ok": "Uğurlu",
"Exit.InvalidArguments": "yanlış arqumentlər",
"Exit.EbootNotFound": "eboot tapılmadı",
"Exit.RuntimeException": "icra zamanı istisna",
"Options.CrashDump.Label": "Qəza yaddaş faylları",
"Options.CrashDump.Desc": "Qəza yaddaş fayllarını icra jurnalının yanında saxla (yalnız Windows x64). Fayllar böyük ola və şəxsi məlumatlar ehtiva edə bilər.",
"Options.Env.PerformanceProfile.Desc": "Növbəti oyun başladıqda performans vaxtlarını və FPS-i jurnala yaz.\nBu, performansı azalda bilər.",
"Options.Env.PerformanceFrameTrace.Desc": "Son 4 096 təqdimat intervalını saxla və oyun bağlandıqda jurnala yaz.\nPerformans profilləşdirilməsi tələb olunur. İzi saxlamaq üçün oyunu normal qaydada bağlayın.",
"Options.Env.StrictCompute.Desc": "Hesablama və çəkim üçün şeyderin rədd edilməsi yoxlamalarında dayan (standart).\nTəsirlənən əməliyyatları ötürmək üçün söndürün. Görüntülər itə, xətalar və ya donmalar baş verə bilər. Dəyişiklikdən sonra oyunu yenidən başladın.",
"Options.OverlayEnabled.Label": "Performans göstəriciləri",
"Options.OverlayEnabled.Desc": "Performans statistikasını göstər. F1 cari icra üçün göstəriciləri açır və bağlayır. SHARPEMU_OVERLAY=0 onları başlanğıcda gizlədir.",
"Options.OverlayMode.Label": "Göstərmə rejimi",
"Options.OverlayMode.Desc": "Tam rejim bütün sayğacları göstərir. Minimal və başlıq sətri rejimləri FPS, CPU, GPU istifadəsini və keçən vaxtı göstərir. GPU istifadəsi Windows-da hər proses üçün ölçülür; digər sistemlərdə N/A göstərilir.",
"Options.OverlayCorner.Label": "Ekran küncü",
"Options.OverlayCorner.Desc": "Saxlanacaq küncü seçin. F2 yalnız cari icra üçün küncləri dəyişir. Bu, başlıq sətrindəki statistikanın yerini dəyişmir.",
"Options.OverlayMode.Full": "Tam",
"Options.OverlayMode.Minimal": "Minimal",
"Options.OverlayMode.TitleBar": "Başlıq sətri (minimal)",
"Options.OverlayCorner.TopLeft": "Yuxarı sol",
"Options.OverlayCorner.TopRight": "Yuxarı sağ",
"Options.OverlayCorner.BottomRight": "Aşağı sağ",
"Options.OverlayCorner.BottomLeft": "Aşağı sol",
"Exit.EmulationError": "emulyasiya xətası",
"Exit.Unknown": "naməlum",
"Status.EmulatorLocating": "Emulyator: axtarılır…",
"Status.EmulatorPath": "Emulyator: {0}",
"Status.EmulatorNotFound": "Emulyator: SharpEmu icra faylı tapılmadı. Əvvəlcə SharpEmu.CLI layihəsini yığın.",
"Status.ScanningLibrary": "Kitabxana yoxlanılır…",
"Status.AddFolderPrompt": "Kitabxananı doldurmaq üçün oyun qovluğu əlavə edin.",
"Status.LibraryScanned": "Kitabxana yoxlanıldı: {0} oyun, {1} qovluqda.",
"Status.CouldNotOpenFolder": "Qovluğu açmaq mümkün olmadı: {0}",
"Status.CopiedToClipboard": "{0} mübadilə buferinə kopyalandı.",
"Status.RemovedFromLibrary": "“{0}” kitabxanadan çıxarıldı. Bərpa etmək üçün qovluğunu yenidən əlavə edin.",
"Status.Running": "İşləyir: {0}",
"Status.Stopping": "Dayandırılır…",
"Status.Idle": "Gözləmədə",
"Clipboard.Path": "Yol",
"Clipboard.TitleId": "Oyunun ID-si",
"Discord.Playing": "{0} oynayır",
"Discord.Browsing": "Kitabxanaya baxır",
"Dialog.ChooseGameFolder": "Oyunların olduğu qovluğu seç",
"Dialog.OpenExecutable": "İşə salmaq üçün icra faylını aç",
"Dialog.PsExecutables": "PS icra faylları",
"Dialog.SaveLogFile": "Jurnal faylının saxlanacağı yeri seç",
"Dialog.PlainTextFiles": "Sadə mətn faylları",
"Dialog.LogFiles": "Jurnal faylları",
"Options.About" : "Haqqında",
"About.Github.Label": "GitHub",
"About.Github.Desc": "Mənbə kodu, problemlər və layihənin inkişafı.",
"About.Github.LatestCommitLabel": "Son kod dəyişikliyi",
"About.Github.LatestCommitDescription": "Əsas budaqdakı son kod dəyişikliyi",
"About.Discord.Label": "Discord",
"About.Discord.Desc": "İcmaya qoşulun, dəstək alın və layihənin inkişafını izləyin.",
"About.GithubButton": "GitHub-da töhfə ver!",
"About.DiscordButton": "Discord icmamıza qoşul!",
"Updater.Auto.Label": "Başlanğıcda yeniləmələri yoxla",
"Updater.Auto.Desc": "Başlanğıcı ləngitmədən GitHub-da yoxlayır.",
"Updater.Label": "Yeniləmələr",
"Updater.Check": "Yeniləmələri yoxla",
"Updater.DownloadRestart": "Endir və yenidən başlat",
"Updater.Status.Ready": "Cari yığım: {0}",
"Updater.Status.Checking": "Yeniləmələr yoxlanılır…",
"Updater.Status.Current": "Ən son versiyadan istifadə edirsiniz ({0}).",
"Updater.Status.Available": "Yeni yığım mövcuddur: {0}",
"Updater.Status.Downloading": "Yeniləmə endirilir… {0}%",
"Updater.Status.Installing": "Yeniləmə quraşdırılır…",
"Updater.Status.Timeout": "Yeniləmələrin yoxlanması üçün ayrılmış 10 saniyə bitdi.",
"Updater.Status.Failed": "Yeniləmələri yoxlamaq mümkün olmadı.",
"Updater.Status.ChecksumFailed": "Endirilmiş yeniləmə SHA-256 yoxlamasından keçmədi.",
"Updater.Status.Unsupported": "Avtomatik yeniləmə üçün Windows, Linux və ya macOS x64 yığımı tələb olunur."
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "Português (Brasil)",
"Options.Env.StrictCompute.Desc": "Parar nas rejeições de shaders de cálculo e desenho (padrão).\nDesative para ignorar o trabalho afetado. Podem ocorrer imagens ausentes, erros ou travamentos. Reinicie o jogo após uma alteração.",
"Options.Env.PerformanceProfile.Desc": "Registre os tempos de desempenho e FPS a partir da próxima execução do jogo.\nIsso pode reduzir o desempenho.",
"Options.Env.PerformanceFrameTrace.Desc": "Guarde os últimos 4.096 intervalos de apresentação e grave-os no log ao encerrar.\nRequer o perfil de desempenho. Feche o jogo normalmente para salvar o rastreamento.",
"Options.OverlayEnabled.Label": "Indicador de desempenho",
"Options.OverlayEnabled.Desc": "Mostrar estatísticas. F1 ativa ou oculta o indicador nesta sessão. SHARPEMU_OVERLAY=0 oculta o indicador ao iniciar.",
"Options.OverlayMode.Label": "Modo de exibição",
"Options.OverlayMode.Desc": "Completo mostra todos os contadores. Mínimo e Barra de título mostram FPS, CPU, uso da GPU e tempo. A GPU é medida por processo no Windows; nos outros sistemas, N/A.",
"Options.OverlayCorner.Label": "Canto da tela",
"Options.OverlayCorner.Desc": "Escolher o canto salvo. F2 muda de canto apenas nesta sessão. Não move as estatísticas da barra de título.",
"Options.OverlayMode.Full": "Completo",
"Options.OverlayMode.Minimal": "Mínimo",
"Options.OverlayMode.TitleBar": "Barra de título (mínimo)",
"Options.OverlayCorner.TopLeft": "Superior esquerdo",
"Options.OverlayCorner.TopRight": "Superior direito",
"Options.OverlayCorner.BottomRight": "Inferior direito",
"Options.OverlayCorner.BottomLeft": "Inferior esquerdo",
"Page.Library": "Biblioteca",
"Page.Options": "Opções",
@ -57,8 +41,9 @@
"Options.Env.LogNp.Desc": "Registra chamadas da biblioteca NP (PlayStation Network) no console.",
"Options.Env.Group.Debug": "Depuração",
"Options.Env.Group.General": "Geral",
"Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames de dentro do emulador.\nPressione F12 enquanto o jogo roda para capturar um frame; as capturas vão para user/logs/capture_logs/<TITLE_ID>.\nExige o RenderDoc instalado. Deixa a GPU mais lenta e trava alguns títulos, então mantenha desativado a menos que esteja depurando.",
"Options.Env.DisableVkImplicitLayers.Desc": "Quando o RenderDoc está ativado, outros overlays conflitantes podem causar device loss. Ativar esta opção pode corrigir o problema.",
"Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames de dentro do emulador.\nPressione F10 enquanto o jogo roda para capturar um frame; as capturas vão para user/logs/capture_logs/<TITLE_ID>.\nExige o RenderDoc instalado. Deixa a GPU mais lenta e trava alguns títulos, então mantenha desativado a menos que esteja depurando.",
"Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixe desativado normalmente. Ative para títulos cujas superfícies desenhadas pela CPU nunca chegam à tela.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Entrega os preâmbulos do buffer de comandos da GPU mesmo quando a fila de destino nunca os recolhe.\nDeixe desativado normalmente. Ative para títulos que travam esperando por uma fence de GPU que nunca sinaliza.",
"Options.Section.Emulation": "EMULAÇÃO",
"Options.Section.Logging": "LOGS",
"Options.Section.Launcher": "INICIALIZADOR",
@ -82,8 +67,6 @@
"Options.TraceImports.Label": "Limite de rastreamento de importações",
"Options.TraceImports.Desc": "Rastreia as primeiras N importações de cada módulo (0 = desativado).",
"Options.CrashDump.Label": "Despejos de falha",
"Options.CrashDump.Desc": "Salva despejos de falha ao lado do log da execução (somente Windows x64). Os arquivos podem ser grandes e conter dados privados.",
"Options.LogToFile.Label": "Salvar log em arquivo",
"Options.LogToFile.Desc": "Copia a saída do emulador para um arquivo de log.",
@ -121,17 +104,6 @@
"Launch.Idle": "Ocioso",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Iniciar",
"Launch.CustomEnv": "Iniciar com variáveis de ambiente personalizadas",
"Launch.CustomEnv.Title": "Iniciar com variáveis de ambiente personalizadas",
"Launch.CustomEnv.Desc": "Digite uma variável de ambiente por linha no formato NOME=VALOR. Os valores personalizados substituem as configurações da GUI e específicas do jogo com o mesmo nome.",
"Launch.CustomEnv.Documentation": "Ver variáveis de ambiente não documentadas ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_EXEMPLO=1",
"Launch.CustomEnv.SaveGame": "Salvar para o jogo",
"Launch.CustomEnv.SaveGlobal": "Salvar globalmente",
"Launch.CustomEnv.Play": "Jogar",
"Launch.CustomEnv.Invalid": "Variável de ambiente inválida: {0}",
"Launch.CustomEnv.SavedGame": "Salvo para este jogo.",
"Launch.CustomEnv.SavedGlobal": "Salvo globalmente.",
"Launch.Stop": "■ Parar",
"Launch.Running": "Em execução — {0}",
"Launch.Stopping": "Encerrando…",
@ -181,7 +153,7 @@
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Participe da comunidade, obtenha suporte e acompanhe o desenvolvimento.",
"About.GithubButton": "Contribua no GitHub!",
"About.DiscordButton": "Junte-se ao nosso Discord!",
"About.DiscordComingSoon": "Em breve",
"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.",
@ -206,13 +178,11 @@
"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.",
"Options.Graphics": "Gráficos",
"Options.Console.Type.Label": "Console",
"Options.Console.Type.Desc": "Modelo de console informado aos jogos.",
"Options.Console.Language.Label": "Idioma do console",
"Options.Console.Language.Desc": "Idioma do sistema informado aos jogos.",
"Options.Section.Rendering": "RENDERIZAÇÃO",
"Options.Section.Display": "TELA",
"Options.RenderResolution.Label": "Resolução interna",
"Options.RenderResolution.Desc": "Renderiza alvos fora da tela abaixo da resolução nativa e amplia na apresentação. Valores menores trocam qualidade de imagem por folga da GPU; entra em vigor na próxima inicialização.",
"Options.RenderResolution.Native": "100% (nativa)",
"Options.WindowMode.Label": "Modo de janela",
"Options.WindowMode.Desc": "Janela normal, área de trabalho sem bordas ou tela cheia exclusiva.",
"Options.WindowMode.Windowed": "Em janela",
@ -235,10 +205,5 @@
"Options.VSync.Desc": "Usa apresentação FIFO para evitar cortes na imagem.",
"Options.Hdr.Label": "Saída HDR",
"Options.Hdr.Desc": "Usa HDR quando a tela selecionada e o backend gráfico oferecem suporte. O modo automático retorna ao SDR.",
"Options.Hdr.Auto": "Automático",
"Options.Env.BinkPlayback.Label": "Reprodução de vídeo Bink",
"Options.Env.BinkPlayback.Desc": "Host reproduz vídeos Bink pela ponte FFmpeg, mas alguns jogos podem apresentar problemas.\nGuest usa o decodificador do próprio motor do jogo.\nSkip ignora os vídeos Bink, o que pode facilitar o desenvolvimento.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Ignorar"
"Options.Hdr.Auto": "Automático"
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "Deutsch",
"Options.Env.StrictCompute.Desc": "Bei Shader-Ablehnungen für Compute und Zeichenbefehle stoppen (Standard).\nAusschalten, um betroffene Arbeit zu überspringen. Fehlende Bilder, Fehler oder Stillstände sind möglich. Spiel nach Änderungen neu starten.",
"Options.Env.PerformanceProfile.Desc": "Leistungszeiten und FPS ab dem nächsten Spielstart protokollieren.\nDies kann die Leistung verringern.",
"Options.Env.PerformanceFrameTrace.Desc": "Die letzten 4.096 Präsentationsintervalle speichern und beim Beenden ins Protokoll schreiben.\nErfordert Leistungsprofilierung. Das Spiel normal schließen, um die Aufzeichnung zu speichern.",
"Options.OverlayEnabled.Label": "Leistungsanzeige",
"Options.OverlayEnabled.Desc": "Leistungsdaten anzeigen. F1 schaltet die Anzeige nur für diesen Lauf um. SHARPEMU_OVERLAY=0 blendet sie beim Start aus.",
"Options.OverlayMode.Label": "Anzeigemodus",
"Options.OverlayMode.Desc": "Vollständig zeigt alle Zähler. Minimal und Titelleiste zeigen FPS, CPU, GPU-Auslastung und Laufzeit. GPU-Auslastung gilt unter Windows pro Prozess; sonst N/A.",
"Options.OverlayCorner.Label": "Bildschirmecke",
"Options.OverlayCorner.Desc": "Gespeicherte Ecke wählen. F2 wechselt die Ecke nur für diesen Lauf. Die Titelleistenanzeige bleibt unverändert.",
"Options.OverlayMode.Full": "Vollständig",
"Options.OverlayMode.Minimal": "Minimal",
"Options.OverlayMode.TitleBar": "Titelleiste (minimal)",
"Options.OverlayCorner.TopLeft": "Oben links",
"Options.OverlayCorner.TopRight": "Oben rechts",
"Options.OverlayCorner.BottomRight": "Unten rechts",
"Options.OverlayCorner.BottomLeft": "Unten links",
"Page.Library": "Bibliothek",
"Page.Options": "Optionen",
@ -69,8 +53,6 @@
"Options.TraceImports.Label": "Import-Trace-Limit",
"Options.TraceImports.Desc": "Die ersten N Imports pro Modul verfolgen (0 = aus).",
"Options.CrashDump.Label": "Absturzabbilder",
"Options.CrashDump.Desc": "Speichert Absturzabbilder neben dem Laufprotokoll (nur Windows x64). Die Dateien können groß sein und private Daten enthalten.",
"Options.LogToFile.Label": "In Datei protokollieren",
"Options.LogToFile.Desc": "Emulator-Ausgabe zusätzlich in eine Log-Datei schreiben.",
@ -108,17 +90,6 @@
"Launch.Idle": "Bereit",
"Launch.Console": "≡ Konsole",
"Launch.Launch": "▶ Starten",
"Launch.CustomEnv": "Mit benutzerdefinierten Umgebungsvariablen starten",
"Launch.CustomEnv.Title": "Mit benutzerdefinierten Umgebungsvariablen starten",
"Launch.CustomEnv.Desc": "Gib pro Zeile eine Umgebungsvariable im Format NAME=WERT ein. Benutzerdefinierte Werte überschreiben gleichnamige GUI- und spielspezifische Einstellungen.",
"Launch.CustomEnv.Documentation": "Undokumentierte Umgebungsvariablen anzeigen ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_BEISPIEL=1",
"Launch.CustomEnv.SaveGame": "Für Spiel speichern",
"Launch.CustomEnv.SaveGlobal": "Global speichern",
"Launch.CustomEnv.Play": "Spielen",
"Launch.CustomEnv.Invalid": "Ungültige Umgebungsvariable: {0}",
"Launch.CustomEnv.SavedGame": "Für dieses Spiel gespeichert.",
"Launch.CustomEnv.SavedGlobal": "Global gespeichert.",
"Launch.Stop": "■ Stoppen",
"Launch.Running": "Läuft -- {0}",
"Launch.Stopping": "Wird beendet…",
@ -176,8 +147,9 @@
"Options.Env.LogNp.Desc": "NP-Bibliotheksaufrufe (PlayStation Network) in der Konsole protokollieren.",
"Options.Env.Group.Debug": "Debug",
"Options.Env.Group.General": "Allgemein",
"Options.Env.RenderDoc.Desc": "Lädt die RenderDoc-In-Application-API, damit Frames aus dem Emulator heraus aufgezeichnet werden können.\nDrücke F12 während das Spiel läuft, um ein Frame aufzuzeichnen; Aufzeichnungen landen in user/logs/capture_logs/<TITLE_ID>.\nErfordert eine RenderDoc-Installation. Verlangsamt die GPU und lässt manche Titel hängen, lass es also aus, wenn du nicht debuggst.",
"Options.Env.DisableVkImplicitLayers.Desc": "Wenn RenderDoc aktiviert ist, können andere kollidierende Overlays einen Device Loss verursachen. Das Aktivieren dieser Option kann das Problem beheben.",
"Options.Env.RenderDoc.Desc": "Lädt die RenderDoc-In-Application-API, damit Frames aus dem Emulator heraus aufgezeichnet werden können.\nDrücke F10 während das Spiel läuft, um ein Frame aufzuzeichnen; Aufzeichnungen landen in user/logs/capture_logs/<TITLE_ID>.\nErfordert eine RenderDoc-Installation. Verlangsamt die GPU und lässt manche Titel hängen, lass es also aus, wenn du nicht debuggst.",
"Options.Env.GuestImageCpuSync.Desc": "Gast-Oberflächen neu hochladen, die der eigene CPU-Code des Spiels überschreibt.\nNormalerweise aus lassen. Für Titel aktivieren, deren CPU-gezeichnete Oberflächen nie auf dem Bildschirm erscheinen.\nKostet Leistung und verursacht bei einigen Titeln wie GTA V Regressionen.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Liefert GPU-Befehlspuffer-Präambeln auch dann aus, wenn die Zielwarteschlange sie nie abholt.\nNormalerweise aus lassen. Für Titel aktivieren, die beim Warten auf einen GPU-Fence hängen bleiben, der nie signalisiert.",
"Common.Save": "Speichern",
"Common.Cancel": "Abbrechen",
"Options.About": "Über",
@ -188,7 +160,7 @@
"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!",
"About.DiscordComingSoon": "Demnächst verfügbar",
"Updater.Auto.Label": "Beim Start nach Updates suchen",
"Updater.Auto.Desc": "Fragt GitHub ab, ohne den Start zu verzögern.",
"Updater.Label": "Updates",
@ -205,13 +177,11 @@
"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.",
"Options.Graphics": "Grafik",
"Options.Console.Type.Label": "Konsole",
"Options.Console.Type.Desc": "Konsolenmodell, das Spielen gemeldet wird.",
"Options.Console.Language.Label": "Konsolensprache",
"Options.Console.Language.Desc": "Systemsprache, die Spielen gemeldet wird.",
"Options.Section.Rendering": "DARSTELLUNG",
"Options.Section.Display": "ANZEIGE",
"Options.RenderResolution.Label": "Interne Auflösung",
"Options.RenderResolution.Desc": "Offscreen-Ziele unterhalb der nativen Auflösung rendern und bei der Ausgabe hochskalieren. Niedrigere Werte tauschen Bildqualität gegen GPU-Reserven; wird beim nächsten Start wirksam.",
"Options.RenderResolution.Native": "100 % (nativ)",
"Options.WindowMode.Label": "Fenstermodus",
"Options.WindowMode.Desc": "Normales Fenster, randloser Desktop oder exklusiver Vollbildmodus.",
"Options.WindowMode.Windowed": "Fenster",
@ -234,10 +204,5 @@
"Options.VSync.Desc": "FIFO-Präsentation für eine Ausgabe ohne Tearing verwenden.",
"Options.Hdr.Label": "HDR-Ausgabe",
"Options.Hdr.Desc": "HDR verwenden, wenn die ausgewählte Anzeige und das Grafik-Backend es unterstützen. Automatisch fällt auf SDR zurück.",
"Options.Hdr.Auto": "Automatisch",
"Options.Env.BinkPlayback.Label": "Bink-Videowiedergabe",
"Options.Env.BinkPlayback.Desc": "Host spielt Bink-Videos über die FFmpeg-Brücke ab, bei einigen Spielen können jedoch Probleme auftreten.\nGuest verwendet den spieleigenen Decoder.\nSkip überspringt Bink-Videos, was die Entwicklung erleichtern kann.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Überspringen"
"Options.Hdr.Auto": "Automatisch"
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "Dansk",
"Options.Env.StrictCompute.Desc": "Stop ved shaderafvisninger for beregning og tegning (standard).\nSlå fra for at springe det berørte arbejde over. Manglende billeder, fejl eller stop kan forekomme. Genstart spillet efter en ændring.",
"Options.Env.PerformanceProfile.Desc": "Log ydelsestider og FPS fra næste spilstart.\nDette kan reducere ydelsen.",
"Options.Env.PerformanceFrameTrace.Desc": "Gem de sidste 4.096 præsentationsintervaller, og skriv dem til loggen ved afslutning.\nKræver ydelsesprofilering. Luk spillet normalt for at gemme sporingen.",
"Options.OverlayEnabled.Label": "Ydelsesvisning",
"Options.OverlayEnabled.Desc": "Vis ydelsesdata. F1 slår visningen til eller fra i denne session. SHARPEMU_OVERLAY=0 skjuler den ved start.",
"Options.OverlayMode.Label": "Visningstilstand",
"Options.OverlayMode.Desc": "Fuld viser alle tællere. Minimal og Titellinje viser FPS, CPU, GPU-forbrug og forløbet tid. GPU-forbrug måles pr. proces i Windows; ellers N/A.",
"Options.OverlayCorner.Label": "Skærmhjørne",
"Options.OverlayCorner.Desc": "Vælg det gemte hjørne. F2 skifter hjørne kun i denne session. Statistik i titellinjen flyttes ikke.",
"Options.OverlayMode.Full": "Fuld",
"Options.OverlayMode.Minimal": "Minimal",
"Options.OverlayMode.TitleBar": "Titellinje (minimal)",
"Options.OverlayCorner.TopLeft": "Øverst til venstre",
"Options.OverlayCorner.TopRight": "Øverst til højre",
"Options.OverlayCorner.BottomRight": "Nederst til højre",
"Options.OverlayCorner.BottomLeft": "Nederst til venstre",
"Page.Library": "Bibliotek",
"Page.Options": "Indstillinger",
@ -69,8 +53,6 @@
"Options.TraceImports.Label": "Grænse for import-trace",
"Options.TraceImports.Desc": "Spor de første N imports pr. modul (0 = fra).",
"Options.CrashDump.Label": "Nedbrudsdumps",
"Options.CrashDump.Desc": "Gem nedbrudsdumps ved siden af kørselsloggen (kun Windows x64). Filerne kan være store og indeholde private data.",
"Options.LogToFile.Label": "Log til fil",
"Options.LogToFile.Desc": "Spejl emulatorens output til en logfil.",
@ -108,17 +90,6 @@
"Launch.Idle": "Inaktiv",
"Launch.Console": "≡ Konsol",
"Launch.Launch": "▶ Start",
"Launch.CustomEnv": "Start med brugerdefinerede miljøvariabler",
"Launch.CustomEnv.Title": "Start med brugerdefinerede miljøvariabler",
"Launch.CustomEnv.Desc": "Indtast én miljøvariabel pr. linje som NAVN=VÆRDI. Brugerdefinerede værdier tilsidesætter GUI- og spilspecifikke indstillinger med samme navn.",
"Launch.CustomEnv.Documentation": "Vis udokumenterede miljøvariabler ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_EKSEMPEL=1",
"Launch.CustomEnv.SaveGame": "Gem for spil",
"Launch.CustomEnv.SaveGlobal": "Gem globalt",
"Launch.CustomEnv.Play": "Spil",
"Launch.CustomEnv.Invalid": "Ugyldig miljøvariabel: {0}",
"Launch.CustomEnv.SavedGame": "Gemt for dette spil.",
"Launch.CustomEnv.SavedGlobal": "Gemt globalt.",
"Launch.Stop": "■ Stop",
"Launch.Running": "Kører — {0}",
"Launch.Stopping": "Stopper…",
@ -176,8 +147,9 @@
"Options.Env.LogNp.Desc": "Log NP-bibliotekskald (PlayStation Network) til konsollen.",
"Options.Env.Group.Debug": "Fejlfinding",
"Options.Env.Group.General": "Generelt",
"Options.Env.RenderDoc.Desc": "Indlæser RenderDocs in-application-API, så frames kan optages inde fra emulatoren.\nTryk på F12 mens spillet kører for at optage ét frame; optagelser havner i user/logs/capture_logs/<TITLE_ID>.\nKræver at RenderDoc er installeret. Gør GPU'en langsommere og får nogle titler til at hænge, så lad den være slået fra, medmindre du fejlfinder.",
"Options.Env.DisableVkImplicitLayers.Desc": "Når RenderDoc er aktiveret, kan andre konflikterende overlays forårsage device loss. Aktivering af denne indstilling kan løse problemet.",
"Options.Env.RenderDoc.Desc": "Indlæser RenderDocs in-application-API, så frames kan optages inde fra emulatoren.\nTryk på F10 mens spillet kører for at optage ét frame; optagelser havner i user/logs/capture_logs/<TITLE_ID>.\nKræver at RenderDoc er installeret. Gør GPU'en langsommere og får nogle titler til at hænge, så lad den være slået fra, medmindre du fejlfinder.",
"Options.Env.GuestImageCpuSync.Desc": "Genindlæs gæsteoverflader, som spillets egen CPU-kode omskriver.\nLad den være slået fra normalt. Slå til for titler, hvis CPU-tegnede overflader aldrig når skærmen.\nKoster ydeevne og giver regressioner i nogle titler, såsom GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Leverer GPU-kommandobuffer-præambler, selv når målkøen aldrig henter dem.\nLad den være slået fra normalt. Slå til for titler, der hænger og venter på en GPU-fence, der aldrig signalerer.",
"Common.Save": "Gem",
"Common.Cancel": "Annuller",
"Options.About": "Om",
@ -188,7 +160,7 @@
"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": "Deltag i vores Discord!",
"About.DiscordComingSoon": "Kommer snart",
"Updater.Auto.Label": "Søg efter opdateringer ved start",
"Updater.Auto.Desc": "Tjekker GitHub uden at forsinke opstarten.",
"Updater.Label": "Opdateringer",
@ -205,13 +177,11 @@
"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.",
"Options.Graphics": "Grafik",
"Options.Console.Type.Label": "Konsol",
"Options.Console.Type.Desc": "Konsolmodel der rapporteres til spil.",
"Options.Console.Language.Label": "Konsolsprog",
"Options.Console.Language.Desc": "Systemsprog der rapporteres til spil.",
"Options.Section.Rendering": "GENGIVELSE",
"Options.Section.Display": "SKÆRM",
"Options.RenderResolution.Label": "Intern opløsning",
"Options.RenderResolution.Desc": "Render offscreen-mål under den oprindelige opløsning, og opskaler dem ved visning. Lavere værdier bytter billedkvalitet for GPU-kapacitet; træder i kraft ved næste start.",
"Options.RenderResolution.Native": "100 % (oprindelig)",
"Options.WindowMode.Label": "Vinduestilstand",
"Options.WindowMode.Desc": "Normalt vindue, kantløst skrivebord eller eksklusiv fuldskærm.",
"Options.WindowMode.Windowed": "Vindue",
@ -234,10 +204,5 @@
"Options.VSync.Desc": "Brug FIFO-præsentation for output uden tearing.",
"Options.Hdr.Label": "HDR-output",
"Options.Hdr.Desc": "Brug HDR, når den valgte skærm og grafik-backend understøtter det. Automatisk falder tilbage til SDR.",
"Options.Hdr.Auto": "Automatisk",
"Options.Env.BinkPlayback.Label": "Bink-videoafspilning",
"Options.Env.BinkPlayback.Desc": "Host afspiller Bink-videoer via FFmpeg-broen, men nogle spil kan opleve problemer.\nGuest bruger spilmotorens egen dekoder.\nSkip springer Bink-videoer over, hvilket kan gøre udviklingen nemmere.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Spring over"
"Options.Hdr.Auto": "Automatisk"
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "Eesti",
"Options.Env.StrictCompute.Desc": "Peata arvutus- ja joonistusvarjutajate tagasilükkamisel (vaikimisi).\nLülita välja, et mõjutatud töö vahele jätta. Võimalikud on puuduvad pildid, vead või hangumine. Pärast muudatust taaskäivita mäng.",
"Options.Env.PerformanceProfile.Desc": "Logi jõudlusajad ja FPS alates järgmisest mängu käivitamisest.\nSee võib jõudlust vähendada.",
"Options.Env.PerformanceFrameTrace.Desc": "Säilita viimased 4096 esitamisintervalli ja kirjuta need sulgemisel logisse.\nVajab jõudluse profileerimist. Jälje salvestamiseks sulge mäng tavapäraselt.",
"Options.OverlayEnabled.Label": "Jõudlusnäidik",
"Options.OverlayEnabled.Desc": "Kuva jõudlusandmed. F1 lülitab näidiku selleks seansiks sisse või välja. SHARPEMU_OVERLAY=0 peidab selle käivitamisel.",
"Options.OverlayMode.Label": "Kuvarežiim",
"Options.OverlayMode.Desc": "Täielik kuvab kõik loendurid. Minimaalne ja Tiitliriba kuvavad FPS-i, CPU, GPU kasutuse ja kestuse. GPU kasutus on Windowsis protsessipõhine; mujal N/A.",
"Options.OverlayCorner.Label": "Ekraaninurk",
"Options.OverlayCorner.Desc": "Vali salvestatav nurk. F2 vahetab nurka ainult selleks seansiks. Tiitliriba näitu see ei liiguta.",
"Options.OverlayMode.Full": "Täielik",
"Options.OverlayMode.Minimal": "Minimaalne",
"Options.OverlayMode.TitleBar": "Tiitliriba (minimaalne)",
"Options.OverlayCorner.TopLeft": "Ülal vasakul",
"Options.OverlayCorner.TopRight": "Ülal paremal",
"Options.OverlayCorner.BottomRight": "All paremal",
"Options.OverlayCorner.BottomLeft": "All vasakul",
"Page.Library": "Teek",
"Page.Options": "Seaded",
@ -62,8 +46,9 @@
"Options.Env.LogNp.Desc": "Logi konsooli NP (PlayStation Network) teegi kutsed.",
"Options.Env.Group.Debug": "Silumine",
"Options.Env.Group.General": "Üldine",
"Options.Env.RenderDoc.Desc": "Laadib RenderDoc'i rakendusesisese API, et kaadreid saaks hõivata otse emulaatorist.\nVajuta mängu käigus F12 ühe kaadri hõivamiseks; hõivatised salvestatakse kausta user/logs/capture_logs/<TITLE_ID>.\nNõuab paigaldatud RenderDoc'i. Aeglustab GPU-d ja põhjustab mõningate mängude hangumist, seega hoia välja lülitatuna, kui sa ei silu.",
"Options.Env.DisableVkImplicitLayers.Desc": "Kui RenderDoc on lubatud, võivad teised konfliktis overlay'd põhjustada device loss'i. Selle valiku lubamine võib probleemi lahendada.",
"Options.Env.RenderDoc.Desc": "Laadib RenderDoc'i rakendusesisese API, et kaadreid saaks hõivata otse emulaatorist.\nVajuta mängu käigus F10 ühe kaadri hõivamiseks; hõivatised salvestatakse kausta user/logs/capture_logs/<TITLE_ID>.\nNõuab paigaldatud RenderDoc'i. Aeglustab GPU-d ja põhjustab mõningate mängude hangumist, seega hoia välja lülitatuna, kui sa ei silu.",
"Options.Env.GuestImageCpuSync.Desc": "Laadi uuesti üles mängupinnad, mida selle enda CPU-kood ümber kirjutab.\nTavaliselt jäta välja lülitatuks. Lülita sisse mängude puhul, mille CPU-ga joonistatud pinnad ei jõua kunagi ekraanile.\nVähendab jõudlust ja halvendab mõne mängu töökindlust (nt GTA V).",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Edasta GPU käsupuhvrite preambulid isegi siis, kui nende sihtjärjekord neid kunagi ei võta.\nTavaliselt jäta välja lülitatuks. Lülita sisse mängude puhul, mis hanguvad oodates GPU-fence'i, mis kunagi ei rakendu.",
"Options.DefaultProfile.Label": "Vaikimisi profiili nimi",
"Options.DefaultProfile.Desc": "Nimi, mida kasutatakse, kui mäng küsib tekstisisestust. Vaikimisi Sharp.",
"Options.Section.Emulation": "EMULATSIOON",
@ -73,11 +58,9 @@
"Options.Section.Display": "KUVA",
"Options.Graphics": "Graafika",
"Options.Console.Type.Label": "Konsool",
"Options.Console.Type.Desc": "Mängudele teatatud konsoolimudel.",
"Options.Console.Language.Label": "Konsooli keel",
"Options.Console.Language.Desc": "Mängudele teatatud süsteemikeel.",
"Options.RenderResolution.Label": "Sisemine eraldusvõime",
"Options.RenderResolution.Desc": "Renderdab ekraanivälised sihid natiivsest madalamal eraldusvõimel ja skaleerib kuvamisel üles. Madalamad väärtused annavad pildikvaliteedi arvelt GPU-le jõudlusvaru; rakendub järgmisel käivitamisel.",
"Options.RenderResolution.Native": "100% (natiivne)",
"Options.WindowMode.Label": "Aknarežiim",
"Options.WindowMode.Desc": "Tavaline aken, äärteta töölauavaade või eksklusiivne täisekraan.",
"Options.WindowMode.Windowed": "Aken",
@ -101,11 +84,6 @@
"Options.Hdr.Label": "HDR-väljund",
"Options.Hdr.Desc": "Kasuta HDR-i, kui valitud kuvar ja graafikaliides seda toetavad. Automaatrežiim lülitub tagasi SDR-ile.",
"Options.Hdr.Auto": "Automaatne",
"Options.Env.BinkPlayback.Label": "Bink-video taasesitus",
"Options.Env.BinkPlayback.Desc": "Host esitab Bink-videoid FFmpegi silla kaudu, kuid mõnes mängus võib esineda probleeme.\nGuest kasutab mängumootori enda dekoodrit.\nSkip jätab Bink-videod vahele, mis võib arendust lihtsustada.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Jäta vahele",
"Options.CpuEngine.Label": "CPU-mootor",
"Options.CpuEngine.Desc": "Käitusmootor, mida kasutatakse mängukoodi käivitamiseks.",
@ -126,8 +104,6 @@
"Options.TraceImports.Label": "Impordi jälituse piir",
"Options.TraceImports.Desc": "Jälita iga mooduli esimesi N importi (0 = väljas).",
"Options.CrashDump.Label": "Krahhitõmmised",
"Options.CrashDump.Desc": "Salvesta krahhitõmmised käivituslogi kõrvale (ainult Windows x64). Failid võivad olla suured ja sisaldada privaatseid andmeid.",
"Options.LogToFile.Label": "Logi faili",
"Options.LogToFile.Desc": "Kopeeri emulaatori väljund logifaili.",
@ -166,17 +142,6 @@
"Launch.Idle": "Ootel",
"Launch.Console": "≡ Konsool",
"Launch.Launch": "▶ Käivita",
"Launch.CustomEnv": "Käivita kohandatud keskkonnamuutujatega",
"Launch.CustomEnv.Title": "Käivita kohandatud keskkonnamuutujatega",
"Launch.CustomEnv.Desc": "Sisesta üks keskkonnamuutuja rea kohta kujul NIMI=VÄÄRTUS. Kohandatud väärtused alistavad sama nimega GUI ja mängupõhised seaded.",
"Launch.CustomEnv.Documentation": "Vaata dokumenteerimata keskkonnamuutujaid ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_NAIDE=1",
"Launch.CustomEnv.SaveGame": "Salvesta mängule",
"Launch.CustomEnv.SaveGlobal": "Salvesta globaalselt",
"Launch.CustomEnv.Play": "Mängi",
"Launch.CustomEnv.Invalid": "Vigane keskkonnamuutuja: {0}",
"Launch.CustomEnv.SavedGame": "Selle mängu jaoks salvestatud.",
"Launch.CustomEnv.SavedGlobal": "Globaalselt salvestatud.",
"Launch.Stop": "■ Peata",
"Launch.Running": "Käib — {0}",
"Launch.Stopping": "Peatamine…",
@ -228,7 +193,7 @@
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Liitu kogukonnaga, saa tuge ja jälgi arendust.",
"About.GithubButton": "Löö GitHubis kaasa!",
"About.DiscordButton": "Liitu meie Discordiga!",
"About.DiscordComingSoon": "Tulekul",
"Updater.Auto.Label": "Kontrolli värskendusi käivitamisel",
"Updater.Auto.Desc": "Kontrollib GitHubist värskendusi käivitamist viivitamata.",

View File

@ -1,21 +1,5 @@
{
"_languageName": "English",
"Options.Env.StrictCompute.Desc": "Stop on shader rejection checks for compute and draws (default).\nTurn off to skip affected work. Missing images, errors, or stalls can occur. Restart the game after a change.",
"Options.Env.PerformanceProfile.Desc": "Log performance timings and FPS from the next game launch.\nThis can reduce performance.",
"Options.Env.PerformanceFrameTrace.Desc": "Keep the last 4,096 presentation intervals and write them to the log at shutdown.\nRequires performance profiling. Close the game normally to save the trace.",
"Options.OverlayEnabled.Label": "Performance display",
"Options.OverlayEnabled.Desc": "Show performance statistics. F1 toggles the display for this run. SHARPEMU_OVERLAY=0 starts it hidden.",
"Options.OverlayMode.Label": "Display mode",
"Options.OverlayMode.Desc": "Full shows all counters. Minimal and Title bar show FPS, CPU, GPU usage, and elapsed time. GPU usage is per-process on Windows; otherwise N/A.",
"Options.OverlayCorner.Label": "Screen corner",
"Options.OverlayCorner.Desc": "Select the saved corner. F2 cycles corners for this run only. This does not move title-bar statistics.",
"Options.OverlayMode.Full": "Full",
"Options.OverlayMode.Minimal": "Minimal",
"Options.OverlayMode.TitleBar": "Title bar (minimal)",
"Options.OverlayCorner.TopLeft": "Top left",
"Options.OverlayCorner.TopRight": "Top right",
"Options.OverlayCorner.BottomRight": "Bottom right",
"Options.OverlayCorner.BottomLeft": "Bottom left",
"Page.Library": "Library",
"Page.Options": "Options",
@ -62,8 +46,9 @@
"Options.Env.LogNp.Desc": "Log NP (PlayStation Network) library calls to the console.",
"Options.Env.Group.Debug": "Debug",
"Options.Env.Group.General": "General",
"Options.Env.RenderDoc.Desc": "Load the RenderDoc in-application API so frames can be captured from inside the emulator.\nPress F12 while the game runs to capture one frame; captures land in user/logs/capture_logs/<TITLE_ID>.\nRequires RenderDoc to be installed. Slows the GPU down and hangs some titles, so leave it off unless you are debugging.",
"Options.Env.DisableVkImplicitLayers.Desc": "When RenderDoc is enabled, other conflicting overlays can cause device loss. Enabling this option may fix it.",
"Options.Env.RenderDoc.Desc": "Load the RenderDoc in-application API so frames can be captured from inside the emulator.\nPress F10 while the game runs to capture one frame; captures land in user/logs/capture_logs/<TITLE_ID>.\nRequires RenderDoc to be installed. Slows the GPU down and hangs some titles, so leave it off unless you are debugging.",
"Options.Env.GuestImageCpuSync.Desc": "Re-upload guest surfaces the game's own CPU code rewrites.\nLeave off normally. Turn on for titles whose CPU-drawn surfaces never reach the screen.\nCosts performance and regresses some titles, such as GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Deliver GPU command-buffer preambles even when their target queue never picks them up.\nLeave off normally. Turn on for titles that hang waiting on a GPU fence that never signals.",
"Options.DefaultProfile.Label": "Default profile name",
"Options.DefaultProfile.Desc": "Name used when a game asks for text input. Defaults to Sharp.",
"Options.Section.Emulation": "EMULATION",
@ -73,11 +58,9 @@
"Options.Section.Display": "DISPLAY",
"Options.Graphics": "Graphics",
"Options.Console.Type.Label": "Console",
"Options.Console.Type.Desc": "Console model exposed to games.",
"Options.Console.Language.Label": "Console language",
"Options.Console.Language.Desc": "System language reported to games.",
"Options.RenderResolution.Label": "Internal resolution",
"Options.RenderResolution.Desc": "Render offscreen targets below native resolution and upscale on present. Lower values trade image quality for GPU headroom; takes effect on next launch.",
"Options.RenderResolution.Native": "100% (native)",
"Options.WindowMode.Label": "Window mode",
"Options.WindowMode.Desc": "Regular window, desktop borderless, or exclusive fullscreen.",
"Options.WindowMode.Windowed": "Windowed",
@ -101,11 +84,6 @@
"Options.Hdr.Label": "HDR output",
"Options.Hdr.Desc": "Use HDR when the selected display and graphics backend support it. Auto falls back to SDR.",
"Options.Hdr.Auto": "Auto",
"Options.Env.BinkPlayback.Label": "Bink video playback",
"Options.Env.BinkPlayback.Desc": "Host plays Bink videos through the FFmpeg bridge, but some games may have issues.\nGuest uses the game engine's own decoder.\nSkip skips Bink videos, which can make development easier.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Skip",
"Options.CpuEngine.Label": "CPU engine",
"Options.CpuEngine.Desc": "Execution engine used to run game code.",
@ -126,8 +104,6 @@
"Options.TraceImports.Label": "Import trace limit",
"Options.TraceImports.Desc": "Trace the first N imports per module (0 = off).",
"Options.CrashDump.Label": "Crash dumps",
"Options.CrashDump.Desc": "Save crash dumps beside the run log (Windows x64 only). Dumps can be large and contain private data.",
"Options.LogToFile.Label": "Log to file",
"Options.LogToFile.Desc": "Mirror emulator output to a log file.",
@ -166,17 +142,6 @@
"Launch.Idle": "Idle",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Launch",
"Launch.CustomEnv": "Launch with custom env",
"Launch.CustomEnv.Title": "Launch with custom env",
"Launch.CustomEnv.Desc": "Enter environment variables one per line as NAME=VALUE. Custom values override matching GUI and per-game environment settings.",
"Launch.CustomEnv.Documentation": "View undocumented environment variables ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_EXAMPLE=1",
"Launch.CustomEnv.SaveGame": "Save for game",
"Launch.CustomEnv.SaveGlobal": "Save globally",
"Launch.CustomEnv.Play": "Play",
"Launch.CustomEnv.Invalid": "Invalid environment variable: {0}",
"Launch.CustomEnv.SavedGame": "Saved for this game.",
"Launch.CustomEnv.SavedGlobal": "Saved globally.",
"Launch.Stop": "■ Stop",
"Launch.Running": "Running — {0}",
"Launch.Stopping": "Stopping…",
@ -228,7 +193,7 @@
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Join the community, get support and follow development.",
"About.GithubButton": "Contribute in GitHub!",
"About.DiscordButton": "Join our Discord!",
"About.DiscordComingSoon": "Coming soon",
"Updater.Auto.Label": "Check for updates on startup",
"Updater.Auto.Desc": "Checks GitHub without delaying startup.",

View File

@ -1,21 +1,5 @@
{
"_languageName": "Español",
"Options.Env.StrictCompute.Desc": "Detener ante rechazos de shaders de cálculo y dibujo (predeterminado).\nDesactivar para omitir el trabajo afectado. Puede haber imágenes ausentes, errores o bloqueos. Reinicia el juego tras un cambio.",
"Options.Env.PerformanceProfile.Desc": "Registra tiempos de rendimiento y FPS desde el próximo inicio del juego.\nEsto puede reducir el rendimiento.",
"Options.Env.PerformanceFrameTrace.Desc": "Conserva los últimos 4096 intervalos de presentación y escríbelos en el registro al salir.\nRequiere el perfil de rendimiento. Cierra el juego normalmente para guardar la traza.",
"Options.OverlayEnabled.Label": "Indicador de rendimiento",
"Options.OverlayEnabled.Desc": "Mostrar estadísticas. F1 activa o desactiva el indicador durante esta sesión. SHARPEMU_OVERLAY=0 lo oculta al iniciar.",
"Options.OverlayMode.Label": "Modo del indicador",
"Options.OverlayMode.Desc": "Completo muestra todos los contadores. Mínimo y Barra de título muestran FPS, CPU, uso de GPU y tiempo. La GPU se mide por proceso en Windows; en otros sistemas, N/A.",
"Options.OverlayCorner.Label": "Esquina de pantalla",
"Options.OverlayCorner.Desc": "Elegir la esquina guardada. F2 cambia de esquina solo en esta sesión. No mueve las estadísticas de la barra de título.",
"Options.OverlayMode.Full": "Completo",
"Options.OverlayMode.Minimal": "Mínimo",
"Options.OverlayMode.TitleBar": "Barra de título (mínimo)",
"Options.OverlayCorner.TopLeft": "Superior izquierda",
"Options.OverlayCorner.TopRight": "Superior derecha",
"Options.OverlayCorner.BottomRight": "Inferior derecha",
"Options.OverlayCorner.BottomLeft": "Inferior izquierda",
"Page.Library": "Biblioteca",
"Page.Options": "Opciones",
@ -69,8 +53,6 @@
"Options.TraceImports.Label": "Límite de trazado de importaciones",
"Options.TraceImports.Desc": "Trazar las primeras N importaciones por módulo (0 = off).",
"Options.CrashDump.Label": "Volcados de fallos",
"Options.CrashDump.Desc": "Guarda los volcados de fallos junto al registro de ejecución (solo Windows x64). Los archivos pueden ser grandes y contener datos privados.",
"Options.LogToFile.Label": "Registrar log en archivo",
"Options.LogToFile.Desc": "Duplicar la salida del emulador en un archivo de logs.",
@ -108,17 +90,6 @@
"Launch.Idle": "Inactivo",
"Launch.Console": "≡ Consola",
"Launch.Launch": "▶ Iniciar",
"Launch.CustomEnv": "Iniciar con variables de entorno personalizadas",
"Launch.CustomEnv.Title": "Iniciar con variables de entorno personalizadas",
"Launch.CustomEnv.Desc": "Introduce una variable de entorno por línea con el formato NOMBRE=VALOR. Los valores personalizados reemplazan los ajustes de la GUI y del juego con el mismo nombre.",
"Launch.CustomEnv.Documentation": "Ver variables de entorno no documentadas ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_EJEMPLO=1",
"Launch.CustomEnv.SaveGame": "Guardar para el juego",
"Launch.CustomEnv.SaveGlobal": "Guardar globalmente",
"Launch.CustomEnv.Play": "Jugar",
"Launch.CustomEnv.Invalid": "Variable de entorno no válida: {0}",
"Launch.CustomEnv.SavedGame": "Guardado para este juego.",
"Launch.CustomEnv.SavedGlobal": "Guardado globalmente.",
"Launch.Stop": "■ Detener",
"Launch.Running": "En ejecución — {0}",
"Launch.Stopping": "Deteniendo…",
@ -170,7 +141,7 @@
"About.Github.LatestCommitDescription": "Último commit en la rama main",
"About.Discord.Desc": "Únete a la comunidad, recibe soporte y sigue el desarrollo.",
"About.GithubButton": "Contribuye en GitHub!",
"About.DiscordButton": "¡Únete a nuestro Discord!",
"About.DiscordComingSoon": "Próximamente",
"Library.Context.GameSettings": "Ajustes del juego…",
"Options.Env.Tab": "Entorno",
@ -186,8 +157,9 @@
"Options.Env.LogNp.Desc": "Registrar en la consola las llamadas a la biblioteca NP (PlayStation Network).",
"Options.Env.Group.Debug": "Depuración",
"Options.Env.Group.General": "General",
"Options.Env.RenderDoc.Desc": "Carga la API in-application de RenderDoc para poder capturar fotogramas desde el emulador.\nPulsa F12 mientras el juego se ejecuta para capturar un fotograma; las capturas se guardan en user/logs/capture_logs/<TITLE_ID>.\nRequiere tener RenderDoc instalado. Ralentiza la GPU y bloquea algunos títulos, así que déjalo desactivado salvo que estés depurando.",
"Options.Env.DisableVkImplicitLayers.Desc": "Cuando RenderDoc está activado, otros overlays en conflicto pueden provocar una pérdida del dispositivo. Activar esta opción puede solucionar el problema.",
"Options.Env.RenderDoc.Desc": "Carga la API in-application de RenderDoc para poder capturar fotogramas desde el emulador.\nPulsa F10 mientras el juego se ejecuta para capturar un fotograma; las capturas se guardan en user/logs/capture_logs/<TITLE_ID>.\nRequiere tener RenderDoc instalado. Ralentiza la GPU y bloquea algunos títulos, así que déjalo desactivado salvo que estés depurando.",
"Options.Env.GuestImageCpuSync.Desc": "Volver a subir las superficies del invitado que reescribe el propio código de CPU del juego.\nDejar desactivado normalmente. Activar en títulos cuyas superficies dibujadas por CPU nunca llegan a la pantalla.\nCuesta rendimiento y causa regresiones en algunos títulos, como GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Entrega los preámbulos del búfer de comandos de la GPU incluso cuando la cola de destino nunca los recoge.\nDejar desactivado normalmente. Activar en títulos que se cuelgan esperando una fence de GPU que nunca se señaliza.",
"Common.Save": "Guardar",
"Common.Cancel": "Cancelar",
"Updater.Auto.Label": "Buscar actualizaciones al iniciar",
@ -206,13 +178,11 @@
"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.",
"Options.Graphics": "Gráficos",
"Options.Console.Type.Label": "Consola",
"Options.Console.Type.Desc": "Modelo de consola que se comunica a los juegos.",
"Options.Console.Language.Label": "Idioma de la consola",
"Options.Console.Language.Desc": "Idioma del sistema que se comunica a los juegos.",
"Options.Section.Rendering": "RENDERIZADO",
"Options.Section.Display": "PANTALLA",
"Options.RenderResolution.Label": "Resolución interna",
"Options.RenderResolution.Desc": "Renderiza objetivos fuera de pantalla por debajo de la resolución nativa y los reescala al presentar. Los valores inferiores sacrifican calidad de imagen para liberar carga de la GPU; se aplica en el próximo inicio.",
"Options.RenderResolution.Native": "100 % (nativa)",
"Options.WindowMode.Label": "Modo de ventana",
"Options.WindowMode.Desc": "Ventana normal, escritorio sin bordes o pantalla completa exclusiva.",
"Options.WindowMode.Windowed": "En ventana",
@ -235,10 +205,5 @@
"Options.VSync.Desc": "Usa presentación FIFO para evitar el desgarro de imagen.",
"Options.Hdr.Label": "Salida HDR",
"Options.Hdr.Desc": "Usa HDR cuando la pantalla seleccionada y el backend gráfico sean compatibles. El modo automático vuelve a SDR.",
"Options.Hdr.Auto": "Automático",
"Options.Env.BinkPlayback.Label": "Reproducción de vídeo Bink",
"Options.Env.BinkPlayback.Desc": "Host reproduce los vídeos Bink mediante el puente FFmpeg, aunque algunos juegos pueden presentar problemas.\nGuest usa el decodificador propio del motor del juego.\nSkip omite los vídeos Bink, lo que puede facilitar el desarrollo.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Omitir"
"Options.Hdr.Auto": "Automático"
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "Français",
"Options.Env.StrictCompute.Desc": "Arrêter lors des rejets de shaders pour le calcul et le dessin (par défaut).\nDésactiver pour ignorer le travail concerné. Des images manquantes, des erreurs ou des blocages sont possibles. Redémarrer le jeu après modification.",
"Options.Env.PerformanceProfile.Desc": "Enregistrer les temps de performance et les FPS au prochain lancement du jeu.\nCela peut réduire les performances.",
"Options.Env.PerformanceFrameTrace.Desc": "Conserver les 4 096 derniers intervalles de présentation et les écrire dans le journal à la fermeture.\nNécessite le profilage des performances. Fermez le jeu normalement pour enregistrer la trace.",
"Options.OverlayEnabled.Label": "Affichage des performances",
"Options.OverlayEnabled.Desc": "Afficher les statistiques. F1 active ou masque l’affichage pour cette session. SHARPEMU_OVERLAY=0 le masque au démarrage.",
"Options.OverlayMode.Label": "Mode d’affichage",
"Options.OverlayMode.Desc": "Complet affiche tous les compteurs. Minimal et Barre de titre affichent FPS, CPU, utilisation GPU et durée. Le GPU est mesuré par processus sous Windows ; sinon N/A.",
"Options.OverlayCorner.Label": "Coin de l’écran",
"Options.OverlayCorner.Desc": "Choisir le coin enregistré. F2 change de coin pour cette session uniquement, sans déplacer les statistiques de la barre de titre.",
"Options.OverlayMode.Full": "Complet",
"Options.OverlayMode.Minimal": "Minimal",
"Options.OverlayMode.TitleBar": "Barre de titre (minimal)",
"Options.OverlayCorner.TopLeft": "En haut à gauche",
"Options.OverlayCorner.TopRight": "En haut à droite",
"Options.OverlayCorner.BottomRight": "En bas à droite",
"Options.OverlayCorner.BottomLeft": "En bas à gauche",
"Page.Library": "Bibliothèque",
"Page.Options": "Options",
@ -57,8 +41,9 @@
"Options.Env.LogNp.Desc": "Journaliser les appels de la bibliothèque NP (PlayStation Network) dans la console.",
"Options.Env.Group.Debug": "Débogage",
"Options.Env.Group.General": "Général",
"Options.Env.RenderDoc.Desc": "Charge l'API in-application de RenderDoc afin de capturer des images depuis l'émulateur.\nAppuyez sur F12 pendant le jeu pour capturer une image ; les captures sont écrites dans user/logs/capture_logs/<TITLE_ID>.\nNécessite l'installation de RenderDoc. Ralentit le GPU et bloque certains jeux : laissez cette option désactivée sauf en cas de débogage.",
"Options.Env.DisableVkImplicitLayers.Desc": "Lorsque RenderDoc est activé, d'autres overlays en conflit peuvent provoquer une perte du périphérique. Activer cette option peut corriger le problème.",
"Options.Env.RenderDoc.Desc": "Charge l'API in-application de RenderDoc afin de capturer des images depuis l'émulateur.\nAppuyez sur F10 pendant le jeu pour capturer une image ; les captures sont écrites dans user/logs/capture_logs/<TITLE_ID>.\nNécessite l'installation de RenderDoc. Ralentit le GPU et bloque certains jeux : laissez cette option désactivée sauf en cas de débogage.",
"Options.Env.GuestImageCpuSync.Desc": "Recharger les surfaces invité que le code CPU du jeu réécrit lui-même.\nLaisser désactivé normalement. Activer pour les titres dont les surfaces dessinées par le CPU n'atteignent jamais l'écran.\nCoûte des performances et provoque des régressions sur certains titres, comme GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Soumettre les préambules de tampon de commandes GPU même quand leur file cible ne les récupère jamais.\nLaisser désactivé normalement. Activer pour les titres qui se figent en attendant une fence GPU qui ne se déclenche jamais.",
"Options.Section.Emulation": "ÉMULATION",
"Options.Section.Logging": "JOURNALISATION",
"Options.Section.Launcher": "LANCEUR",
@ -82,8 +67,6 @@
"Options.TraceImports.Label": "Limite de traçage des imports",
"Options.TraceImports.Desc": "Tracer les N premiers imports de chaque module (0 = désactivé).",
"Options.CrashDump.Label": "Fichiers de vidage après plantage",
"Options.CrashDump.Desc": "Enregistre les fichiers de vidage à côté du journal d’exécution (Windows x64 uniquement). Ces fichiers peuvent être volumineux et contenir des données privées.",
"Options.LogToFile.Label": "Enregistrer dans un fichier",
"Options.LogToFile.Desc": "Copier la sortie de l’émulateur dans un fichier journal.",
@ -121,17 +104,6 @@
"Launch.Idle": "Inactif",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Lancer",
"Launch.CustomEnv": "Lancer avec des variables d'environnement personnalisées",
"Launch.CustomEnv.Title": "Lancer avec des variables d'environnement personnalisées",
"Launch.CustomEnv.Desc": "Saisissez une variable d'environnement par ligne au format NOM=VALEUR. Les valeurs personnalisées remplacent les réglages GUI et spécifiques au jeu portant le même nom.",
"Launch.CustomEnv.Documentation": "Voir les variables d'environnement non documentées ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_EXEMPLE=1",
"Launch.CustomEnv.SaveGame": "Enregistrer pour le jeu",
"Launch.CustomEnv.SaveGlobal": "Enregistrer globalement",
"Launch.CustomEnv.Play": "Jouer",
"Launch.CustomEnv.Invalid": "Variable d'environnement invalide : {0}",
"Launch.CustomEnv.SavedGame": "Enregistré pour ce jeu.",
"Launch.CustomEnv.SavedGlobal": "Enregistré globalement.",
"Launch.Stop": "■ Arrêter",
"Launch.Running": "En cours d’exécution — {0}",
"Launch.Stopping": "Arrêt en cours…",
@ -181,7 +153,7 @@
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Rejoignez la communauté, obtenez de l’aide et suivez le développement.",
"About.GithubButton": "Contribuer sur GitHub !",
"About.DiscordButton": "Rejoignez notre Discord !",
"About.DiscordComingSoon": "Bientôt disponible",
"Library.Context.GameSettings": "Paramètres du jeu…",
"Options.Env.WritableApp0.Desc": "Autoriser les jeux à créer et écrire des fichiers dans leur dossier d’installation.\nNécessaire pour les dumps non empaquetés qui écrivent leurs sauvegardes ou leur configuration sous /app0.",
@ -206,13 +178,11 @@
"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.",
"Options.Graphics": "Graphismes",
"Options.Console.Type.Label": "Console",
"Options.Console.Type.Desc": "Modèle de console signalé aux jeux.",
"Options.Console.Language.Label": "Langue de la console",
"Options.Console.Language.Desc": "Langue système signalée aux jeux.",
"Options.Section.Rendering": "RENDU",
"Options.Section.Display": "AFFICHAGE",
"Options.RenderResolution.Label": "Résolution interne",
"Options.RenderResolution.Desc": "Effectuer le rendu des cibles hors écran sous la résolution native et les mettre à l’échelle lors de l’affichage. Les valeurs inférieures réduisent la qualité d’image pour libérer des ressources GPU ; prend effet au prochain lancement.",
"Options.RenderResolution.Native": "100 % (native)",
"Options.WindowMode.Label": "Mode fenêtre",
"Options.WindowMode.Desc": "Fenêtre standard, bureau sans bordures ou plein écran exclusif.",
"Options.WindowMode.Windowed": "Fenêtré",
@ -235,10 +205,5 @@
"Options.VSync.Desc": "Utiliser la présentation FIFO pour un affichage sans déchirement.",
"Options.Hdr.Label": "Sortie HDR",
"Options.Hdr.Desc": "Utiliser le HDR lorsque l’écran sélectionné et le backend graphique le prennent en charge. Le mode automatique revient au SDR.",
"Options.Hdr.Auto": "Automatique",
"Options.Env.BinkPlayback.Label": "Lecture vidéo Bink",
"Options.Env.BinkPlayback.Desc": "Host lit les vidéos Bink via le pont FFmpeg, mais certains jeux peuvent rencontrer des problèmes.\nGuest utilise le décodeur propre au moteur du jeu.\nSkip ignore les vidéos Bink, ce qui peut faciliter le développement.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Ignorer"
"Options.Hdr.Auto": "Automatique"
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "Hungarian",
"Options.Env.StrictCompute.Desc": "Leállás a számítási és rajzolási shaderek elutasításakor (alapértelmezett).\nKikapcsolva az érintett munka kimarad. Hiányzó képek, hibák vagy lefagyás fordulhat elő. Módosítás után indítsd újra a játékot.",
"Options.Env.PerformanceProfile.Desc": "Teljesítményidők és FPS naplózása a játék következő indításától.\nEz csökkentheti a teljesítményt.",
"Options.Env.PerformanceFrameTrace.Desc": "Az utolsó 4096 megjelenítési intervallum megőrzése és naplóba írása kilépéskor.\nTeljesítményprofilozást igényel. A nyomkövetés mentéséhez zárd be a játékot szabályosan.",
"Options.OverlayEnabled.Label": "Teljesítménykijelzés",
"Options.OverlayEnabled.Desc": "Teljesítményadatok megjelenítése. Az F1 csak erre a futásra kapcsolja a kijelzést. SHARPEMU_OVERLAY=0 esetén rejtve indul.",
"Options.OverlayMode.Label": "Kijelzési mód",
"Options.OverlayMode.Desc": "A Teljes minden számlálót mutat. A Minimális és Címsor az FPS-t, CPU-, GPU-használatot és eltelt időt mutatja. A GPU-adat Windows alatt folyamatonkénti; máshol N/A.",
"Options.OverlayCorner.Label": "Képernyősarok",
"Options.OverlayCorner.Desc": "Válassza ki a mentett sarkot. Az F2 csak erre a futásra vált sarkot. A címsor adatait nem helyezi át.",
"Options.OverlayMode.Full": "Teljes",
"Options.OverlayMode.Minimal": "Minimális",
"Options.OverlayMode.TitleBar": "Címsor (minimális)",
"Options.OverlayCorner.TopLeft": "Bal felső",
"Options.OverlayCorner.TopRight": "Jobb felső",
"Options.OverlayCorner.BottomRight": "Jobb alsó",
"Options.OverlayCorner.BottomLeft": "Bal alsó",
"Page.Library": "Könyvtár",
"Page.Options": "Beállítások",
@ -57,8 +41,9 @@
"Options.Env.LogNp.Desc": "Az NP (PlayStation Network) könyvtárhívásokat naplózza a konzolra.",
"Options.Env.Group.Debug": "Hibakeresés",
"Options.Env.Group.General": "Általános",
"Options.Env.RenderDoc.Desc": "Betölti a RenderDoc alkalmazáson belüli API-ját, így képkockák rögzíthetők az emulátorból.\nNyomd meg az F12-et futó játék közben egy képkocka rögzítéséhez; a felvételek a user/logs/capture_logs/<TITLE_ID> mappába kerülnek.\nTelepített RenderDocot igényel. Lassítja a GPU-t és egyes játékokat lefagyaszt, ezért hagyd kikapcsolva, hacsak nem hibakeresel.",
"Options.Env.DisableVkImplicitLayers.Desc": "Ha a RenderDoc engedélyezve van, más ütköző overlayek device loss hibát okozhatnak. Ennek a beállításnak az engedélyezése megoldhatja a problémát.",
"Options.Env.RenderDoc.Desc": "Betölti a RenderDoc alkalmazáson belüli API-ját, így képkockák rögzíthetők az emulátorból.\nNyomd meg az F10-et futó játék közben egy képkocka rögzítéséhez; a felvételek a user/logs/capture_logs/<TITLE_ID> mappába kerülnek.\nTelepített RenderDocot igényel. Lassítja a GPU-t és egyes játékokat lefagyaszt, ezért hagyd kikapcsolva, hacsak nem hibakeresel.",
"Options.Env.GuestImageCpuSync.Desc": "Újratölti azokat a vendégfelületeket, amelyeket a játék saját CPU-kódja ír felül.\nNormál esetben hagyd kikapcsolva. Kapcsold be azoknál a címeknél, amelyek CPU-val rajzolt felületei sosem jutnak ki a képernyőre.\nTeljesítménybe kerül, és egyes címeknél, például a GTA V-nél regressziót okoz.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "GPU parancspuffer-előtagokat is kézbesít, ha a célsor sosem veszi fel őket.\nNormál esetben hagyd kikapcsolva. Kapcsold be azoknál a címeknél, amelyek egy sosem jelző GPU-fence-re várva lefagynak.",
"Options.Section.Emulation": "EMULÁCIÓ",
"Options.Section.Logging": "LOGOLÁS",
"Options.Section.Launcher": "INDITÓ",
@ -82,8 +67,6 @@
"Options.TraceImports.Label": "Import trace limit",
"Options.TraceImports.Desc": "Az első N darab import nyomon követése modulonként (0 = ki).",
"Options.CrashDump.Label": "Összeomlási memóriaképek",
"Options.CrashDump.Desc": "Az összeomlási memóriaképeket a futási napló mellé menti (csak Windows x64). A fájlok nagyok lehetnek, és személyes adatokat tartalmazhatnak.",
"Options.LogToFile.Label": "Naplozás fájlba",
"Options.LogToFile.Desc": "Az emulátor kimenetének tükrözése egy log fájlba.",
@ -121,17 +104,6 @@
"Launch.Idle": "Tétlen",
"Launch.Console": "≡ Konzol",
"Launch.Launch": "▶ Inditás",
"Launch.CustomEnv": "Indítás egyéni környezeti változókkal",
"Launch.CustomEnv.Title": "Indítás egyéni környezeti változókkal",
"Launch.CustomEnv.Desc": "Soronként egy környezeti változót adj meg NÉV=ÉRTÉK formában. Az egyéni értékek felülírják az azonos nevű GUI- és játékspecifikus beállításokat.",
"Launch.CustomEnv.Documentation": "Nem dokumentált környezeti változók megtekintése ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_PELDA=1",
"Launch.CustomEnv.SaveGame": "Mentés a játékhoz",
"Launch.CustomEnv.SaveGlobal": "Globális mentés",
"Launch.CustomEnv.Play": "Játék",
"Launch.CustomEnv.Invalid": "Érvénytelen környezeti változó: {0}",
"Launch.CustomEnv.SavedGame": "Mentve ehhez a játékhoz.",
"Launch.CustomEnv.SavedGlobal": "Globálisan mentve.",
"Launch.Stop": "■ Leállítás",
"Launch.Running": "Fut — {0}",
"Launch.Stopping": "Leállítás…",
@ -181,7 +153,7 @@
"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.GithubButton": "Járulj hozzá GitHubon!",
"About.DiscordButton": "Csatlakozz a Discordunkhoz!",
"About.DiscordComingSoon": "Hamarosan",
"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.",
@ -206,13 +178,11 @@
"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.",
"Options.Graphics": "Grafika",
"Options.Console.Type.Label": "Konzol",
"Options.Console.Type.Desc": "A játékoknak jelentett konzolmodell.",
"Options.Console.Language.Label": "Konzol nyelve",
"Options.Console.Language.Desc": "A játékoknak jelentett rendszer nyelve.",
"Options.Section.Rendering": "RENDERELÉS",
"Options.Section.Display": "KIJELZŐ",
"Options.RenderResolution.Label": "Belső felbontás",
"Options.RenderResolution.Desc": "A képernyőn kívüli célok renderelése a natívnál kisebb felbontáson, majd felskálázás megjelenítéskor. Az alacsonyabb értékek képminőséget cserélnek GPU-tartalékra; a következő indításkor lép érvénybe.",
"Options.RenderResolution.Native": "100% (natív)",
"Options.WindowMode.Label": "Ablakmód",
"Options.WindowMode.Desc": "Normál ablak, keret nélküli asztal vagy kizárólagos teljes képernyő.",
"Options.WindowMode.Windowed": "Ablakos",
@ -235,10 +205,5 @@
"Options.VSync.Desc": "FIFO megjelenítés használata képtörésmentes kimenethez.",
"Options.Hdr.Label": "HDR-kimenet",
"Options.Hdr.Desc": "HDR használata, ha a kiválasztott kijelző és grafikus backend támogatja. Az automatikus mód SDR-re vált vissza.",
"Options.Hdr.Auto": "Automatikus",
"Options.Env.BinkPlayback.Label": "Bink videólejátszás",
"Options.Env.BinkPlayback.Desc": "A Host az FFmpeg hídon keresztül játssza le a Bink videókat, de egyes játékoknál problémák jelentkezhetnek.\nA Guest a játékmotor saját dekóderét használja.\nA Skip kihagyja a Bink videókat, ami megkönnyítheti a fejlesztést.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Kihagyás"
"Options.Hdr.Auto": "Automatikus"
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "Italiano",
"Options.Env.StrictCompute.Desc": "Arresta in caso di rifiuto degli shader di calcolo e disegno (predefinito).\nDisattiva per saltare il lavoro interessato. Sono possibili immagini mancanti, errori o blocchi. Riavvia il gioco dopo una modifica.",
"Options.Env.PerformanceProfile.Desc": "Registra tempi di prestazione e FPS dal prossimo avvio del gioco.\nQuesto può ridurre le prestazioni.",
"Options.Env.PerformanceFrameTrace.Desc": "Conserva gli ultimi 4096 intervalli di presentazione e scrivili nel registro alla chiusura.\nRichiede la profilazione delle prestazioni. Chiudi il gioco normalmente per salvare la traccia.",
"Options.OverlayEnabled.Label": "Statistiche prestazioni",
"Options.OverlayEnabled.Desc": "Mostra le statistiche. F1 attiva o nasconde la visualizzazione per questa sessione. SHARPEMU_OVERLAY=0 la nasconde all’avvio.",
"Options.OverlayMode.Label": "Modalità di visualizzazione",
"Options.OverlayMode.Desc": "Completa mostra tutti i contatori. Minima e Barra del titolo mostrano FPS, CPU, uso GPU e tempo. La GPU è misurata per processo su Windows; altrimenti N/A.",
"Options.OverlayCorner.Label": "Angolo dello schermo",
"Options.OverlayCorner.Desc": "Scegli l’angolo salvato. F2 cambia angolo solo per questa sessione. Non sposta le statistiche nella barra del titolo.",
"Options.OverlayMode.Full": "Completa",
"Options.OverlayMode.Minimal": "Minima",
"Options.OverlayMode.TitleBar": "Barra del titolo (minima)",
"Options.OverlayCorner.TopLeft": "In alto a sinistra",
"Options.OverlayCorner.TopRight": "In alto a destra",
"Options.OverlayCorner.BottomRight": "In basso a destra",
"Options.OverlayCorner.BottomLeft": "In basso a sinistra",
"Page.Library": "Libreria",
"Page.Options": "Opzioni",
@ -70,8 +54,6 @@
"Options.TraceImports.Desc": "Traccia i primi N import per modulo (0 = disattivato).",
"Options.CrashDump.Label": "Dump degli arresti anomali",
"Options.CrashDump.Desc": "Salva i dump accanto al registro di esecuzione (solo Windows x64). I file possono essere grandi e contenere dati privati.",
"Options.LogToFile.Label": "Salva log su file",
"Options.LogToFile.Desc": "Duplica l'output dell'emulatore in un file di log.",
@ -111,17 +93,6 @@
"Launch.Idle": "Inattivo",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Avvia",
"Launch.CustomEnv": "Avvia con variabili d'ambiente personalizzate",
"Launch.CustomEnv.Title": "Avvia con variabili d'ambiente personalizzate",
"Launch.CustomEnv.Desc": "Inserisci una variabile d'ambiente per riga nel formato NOME=VALORE. I valori personalizzati sostituiscono le impostazioni GUI e specifiche del gioco con lo stesso nome.",
"Launch.CustomEnv.Documentation": "Visualizza le variabili d'ambiente non documentate ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_ESEMPIO=1",
"Launch.CustomEnv.SaveGame": "Salva per il gioco",
"Launch.CustomEnv.SaveGlobal": "Salva globalmente",
"Launch.CustomEnv.Play": "Gioca",
"Launch.CustomEnv.Invalid": "Variabile d'ambiente non valida: {0}",
"Launch.CustomEnv.SavedGame": "Salvato per questo gioco.",
"Launch.CustomEnv.SavedGlobal": "Salvato globalmente.",
"Launch.Stop": "■ Ferma",
"Launch.Running": "In esecuzione — {0}",
"Launch.Stopping": "Arresto in corso…",
@ -181,8 +152,9 @@
"Options.Env.LogNp.Desc": "Registra in console le chiamate alla libreria NP (PlayStation Network).",
"Options.Env.Group.Debug": "Debug",
"Options.Env.Group.General": "Generale",
"Options.Env.RenderDoc.Desc": "Carica l'API in-application di RenderDoc per catturare i frame dall'interno dell'emulatore.\nPremi F12 mentre il gioco è in esecuzione per catturare un frame; le catture finiscono in user/logs/capture_logs/<TITLE_ID>.\nRichiede RenderDoc installato. Rallenta la GPU e blocca alcuni titoli, quindi lascialo disattivato se non stai facendo debug.",
"Options.Env.DisableVkImplicitLayers.Desc": "Quando RenderDoc è attivo, altri overlay in conflitto possono causare una perdita del dispositivo. Abilitare questa opzione può risolvere il problema.",
"Options.Env.RenderDoc.Desc": "Carica l'API in-application di RenderDoc per catturare i frame dall'interno dell'emulatore.\nPremi F10 mentre il gioco è in esecuzione per catturare un frame; le catture finiscono in user/logs/capture_logs/<TITLE_ID>.\nRichiede RenderDoc installato. Rallenta la GPU e blocca alcuni titoli, quindi lascialo disattivato se non stai facendo debug.",
"Options.Env.GuestImageCpuSync.Desc": "Ricarica le superfici guest riscritte dal codice CPU del gioco.\nLasciare disattivato normalmente. Attivare per i titoli le cui superfici disegnate dalla CPU non raggiungono mai lo schermo.\nCosta prestazioni e causa regressioni in alcuni titoli, come GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Consegna i preamboli del buffer di comandi GPU anche quando la coda di destinazione non li preleva mai.\nLasciare disattivato normalmente. Attivare per i titoli che si bloccano in attesa di una fence GPU che non segnala mai.",
"Common.Save": "Salva",
"Common.Cancel": "Annulla",
"Options.About": "Informazioni",
@ -193,7 +165,7 @@
"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!",
"About.DiscordComingSoon": "Prossimamente",
"Updater.Auto.Label": "Controlla aggiornamenti all'avvio",
"Updater.Auto.Desc": "Interroga GitHub senza rallentare l'avvio.",
"Updater.Label": "Aggiornamenti",
@ -210,13 +182,11 @@
"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.",
"Options.Graphics": "Grafica",
"Options.Console.Type.Label": "Console",
"Options.Console.Type.Desc": "Modello di console comunicato ai giochi.",
"Options.Console.Language.Label": "Lingua della console",
"Options.Console.Language.Desc": "Lingua di sistema comunicata ai giochi.",
"Options.Section.Rendering": "RENDERING",
"Options.Section.Display": "SCHERMO",
"Options.RenderResolution.Label": "Risoluzione interna",
"Options.RenderResolution.Desc": "Renderizza i target fuori schermo sotto la risoluzione nativa e li ridimensiona in fase di presentazione. Valori inferiori sacrificano la qualità dell'immagine per lasciare margine alla GPU; ha effetto al prossimo avvio.",
"Options.RenderResolution.Native": "100% (nativa)",
"Options.WindowMode.Label": "Modalità finestra",
"Options.WindowMode.Desc": "Finestra normale, desktop senza bordi o schermo intero esclusivo.",
"Options.WindowMode.Windowed": "In finestra",
@ -239,10 +209,5 @@
"Options.VSync.Desc": "Usa la presentazione FIFO per evitare lo screen tearing.",
"Options.Hdr.Label": "Output HDR",
"Options.Hdr.Desc": "Usa HDR quando lo schermo selezionato e il backend grafico lo supportano. La modalità automatica torna a SDR.",
"Options.Hdr.Auto": "Automatico",
"Options.Env.BinkPlayback.Label": "Riproduzione video Bink",
"Options.Env.BinkPlayback.Desc": "Host riproduce i video Bink tramite il bridge FFmpeg, ma alcuni giochi potrebbero avere problemi.\nGuest usa il decoder del motore di gioco.\nSkip salta i video Bink, facilitando lo sviluppo.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Salta"
"Options.Hdr.Auto": "Automatico"
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "日本語",
"Options.Env.StrictCompute.Desc": "計算と描画のシェーダー拒否時に停止します(既定)。\n無効にすると該当処理をスキップします。画像の欠落、エラー、停止が発生する場合があります。変更後はゲームを再起動してください。",
"Options.Env.PerformanceProfile.Desc": "次回のゲーム起動から処理時間とFPSを記録します。\nパフォーマンスが低下する場合があります。",
"Options.Env.PerformanceFrameTrace.Desc": "直近4,096件の表示間隔を保持し、終了時にログへ書き込みます。\nパフォーマンス計測が必要です。記録を保存するにはゲームを通常の手順で終了してください。",
"Options.OverlayEnabled.Label": "パフォーマンス表示",
"Options.OverlayEnabled.Desc": "統計を表示します。F1で今回の実行中のみ表示を切り替えます。SHARPEMU_OVERLAY=0では非表示で開始します。",
"Options.OverlayMode.Label": "表示モード",
"Options.OverlayMode.Desc": "詳細はすべてのカウンターを表示します。最小とタイトルバーはFPS、CPU、GPU使用率、経過時間を表示します。GPU使用率はWindowsのプロセス単位の値です。他の環境ではN/Aを表示します。",
"Options.OverlayCorner.Label": "画面の隅",
"Options.OverlayCorner.Desc": "保存する位置を選びます。F2は今回の実行中のみ位置を切り替えます。タイトルバーの統計は移動しません。",
"Options.OverlayMode.Full": "詳細",
"Options.OverlayMode.Minimal": "最小",
"Options.OverlayMode.TitleBar": "タイトルバー(最小)",
"Options.OverlayCorner.TopLeft": "左上",
"Options.OverlayCorner.TopRight": "右上",
"Options.OverlayCorner.BottomRight": "右下",
"Options.OverlayCorner.BottomLeft": "左下",
"Page.Library": "ライブラリ",
"Page.Options": "オプション",
@ -69,8 +53,6 @@
"Options.TraceImports.Label": "インポートトレース制限",
"Options.TraceImports.Desc": "各モジュールの最初のN個のインポートをトレースします(0 = 無効)。",
"Options.CrashDump.Label": "クラッシュダンプ",
"Options.CrashDump.Desc": "実行ログと同じ場所にクラッシュダンプを保存します(Windows x64 のみ)。ダンプは大きくなることがあり、個人情報を含む場合があります。",
"Options.LogToFile.Label": "ファイルに保存",
"Options.LogToFile.Desc": "エミュレータの出力をログファイルにコピーします。",
@ -108,17 +90,6 @@
"Launch.Idle": "待機中",
"Launch.Console": "≡ コンソール",
"Launch.Launch": "▶ 起動",
"Launch.CustomEnv": "カスタム環境変数で起動",
"Launch.CustomEnv.Title": "カスタム環境変数で起動",
"Launch.CustomEnv.Desc": "環境変数を 1 行に 1 つ、NAME=VALUE 形式で入力します。カスタム値は、同名の GUI およびゲーム固有の環境設定を上書きします。",
"Launch.CustomEnv.Documentation": "未文書化の環境変数を表示 ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_EXAMPLE=1",
"Launch.CustomEnv.SaveGame": "ゲーム用に保存",
"Launch.CustomEnv.SaveGlobal": "全体設定として保存",
"Launch.CustomEnv.Play": "プレイ",
"Launch.CustomEnv.Invalid": "無効な環境変数: {0}",
"Launch.CustomEnv.SavedGame": "このゲーム用に保存しました。",
"Launch.CustomEnv.SavedGlobal": "全体設定として保存しました。",
"Launch.Stop": "■ 停止",
"Launch.Running": "実行中 — {0}",
"Launch.Stopping": "停止中…",
@ -176,8 +147,9 @@
"Options.Env.LogNp.Desc": "NP(PlayStation Network)ライブラリの呼び出しをコンソールに記録します。",
"Options.Env.Group.Debug": "デバッグ",
"Options.Env.Group.General": "一般",
"Options.Env.RenderDoc.Desc": "RenderDoc のアプリ内 API を読み込み、エミュレーター内からフレームをキャプチャできるようにします。\nゲーム実行中に F12 を押すと 1 フレームをキャプチャします。保存先は user/logs/capture_logs/<TITLE_ID> です。\nRenderDoc のインストールが必要です。GPU が遅くなり一部のタイトルはハングするため、デバッグ時以外はオフのままにしてください。",
"Options.Env.DisableVkImplicitLayers.Desc": "RenderDoc が有効な場合、競合する他のオーバーレイによって device loss が発生することがあります。この設定を有効にすると解決する場合があります。",
"Options.Env.RenderDoc.Desc": "RenderDoc のアプリ内 API を読み込み、エミュレーター内からフレームをキャプチャできるようにします。\nゲーム実行中に F10 を押すと 1 フレームをキャプチャします。保存先は user/logs/capture_logs/<TITLE_ID> です。\nRenderDoc のインストールが必要です。GPU が遅くなり一部のタイトルはハングするため、デバッグ時以外はオフのままにしてください。",
"Options.Env.GuestImageCpuSync.Desc": "ゲーム自身の CPU コードが書き換えるゲスト表面を再アップロードします。\n通常はオフのままにしてください。CPU で描画した表面が画面に反映されないタイトルで有効にします。\n性能を犠牲にし、GTA V など一部のタイトルでは不具合が生じます。",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "対象キューが受け取らない場合でも GPU コマンドバッファのプリアンブルを配信します。\n通常はオフのままにしてください。決してシグナルされない GPU フェンスを待ってハングするタイトルで有効にします。",
"Common.Save": "保存",
"Common.Cancel": "キャンセル",
"Options.About": "情報",
@ -188,7 +160,7 @@
"About.Discord.Label": "Discord",
"About.Discord.Desc": "コミュニティに参加して、サポートを受けたり開発を追いかけたりしましょう。",
"About.GithubButton": "GitHubで貢献しよう!",
"About.DiscordButton": "Discordに参加しよう!",
"About.DiscordComingSoon": "近日公開",
"Updater.Auto.Label": "起動時にアップデートを確認",
"Updater.Auto.Desc": "起動を遅らせずにGitHubへ確認します。",
"Updater.Label": "アップデート",
@ -205,13 +177,11 @@
"Updater.Status.ChecksumFailed": "ダウンロードしたアップデートはSHA-256検証に失敗しました。",
"Updater.Status.Unsupported": "自動アップデートにはWindows、Linux、またはmacOSのx64ビルドが必要です。",
"Options.Graphics": "グラフィックス",
"Options.Console.Type.Label": "コンソール",
"Options.Console.Type.Desc": "ゲームに通知するコンソールモデル。",
"Options.Console.Language.Label": "コンソールの言語",
"Options.Console.Language.Desc": "ゲームに通知するシステム言語。",
"Options.Section.Rendering": "レンダリング",
"Options.Section.Display": "ディスプレイ",
"Options.RenderResolution.Label": "内部解像度",
"Options.RenderResolution.Desc": "ネイティブ解像度より低い解像度でオフスクリーンターゲットを描画し、表示時にアップスケールします。値を下げると画質と引き換えにGPU負荷を軽減します。次回起動時に適用されます。",
"Options.RenderResolution.Native": "100%(ネイティブ)",
"Options.WindowMode.Label": "ウィンドウモード",
"Options.WindowMode.Desc": "通常ウィンドウ、デスクトップのボーダーレス、または排他フルスクリーン。",
"Options.WindowMode.Windowed": "ウィンドウ",
@ -234,10 +204,5 @@
"Options.VSync.Desc": "ティアリングのない表示のためFIFOプレゼンテーションを使用します。",
"Options.Hdr.Label": "HDR出力",
"Options.Hdr.Desc": "選択したディスプレイとグラフィックスバックエンドが対応している場合にHDRを使用します。自動ではSDRにフォールバックします。",
"Options.Hdr.Auto": "自動",
"Options.Env.BinkPlayback.Label": "Bink 動画再生",
"Options.Env.BinkPlayback.Desc": "Host は FFmpeg ブリッジ経由で Bink 動画を再生しますが、一部のゲームで問題が発生する場合があります。\nGuest はゲームエンジン独自のデコーダーを使用します。\nSkip は Bink 動画をスキップし、開発を容易にする場合があります。",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "スキップ"
"Options.Hdr.Auto": "自動"
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "한국어",
"Options.Env.StrictCompute.Desc": "컴퓨트 및 그리기 셰이더 거부 시 중지합니다(기본값).\n끄면 해당 작업을 건너뜁니다. 이미지 누락, 오류 또는 멈춤이 발생할 수 있습니다. 변경 후 게임을 다시 시작하세요.",
"Options.Env.PerformanceProfile.Desc": "다음 게임 실행부터 성능 시간과 FPS를 기록합니다.\n성능이 저하될 수 있습니다.",
"Options.Env.PerformanceFrameTrace.Desc": "최근 4,096개의 화면 표시 간격을 보관하고 종료 시 로그에 기록합니다.\n성능 프로파일링이 필요합니다. 추적을 저장하려면 게임을 정상적으로 종료하세요.",
"Options.OverlayEnabled.Label": "성능 표시",
"Options.OverlayEnabled.Desc": "성능 통계를 표시합니다. F1은 이번 실행에서만 표시를 켜거나 끕니다. SHARPEMU_OVERLAY=0이면 숨긴 상태로 시작합니다.",
"Options.OverlayMode.Label": "표시 모드",
"Options.OverlayMode.Desc": "전체는 모든 카운터를 표시합니다. 최소와 제목 표시줄은 FPS, CPU, GPU 사용률, 경과 시간을 표시합니다. GPU 사용률은 Windows에서 프로세스별로 측정하며 다른 환경에서는 N/A입니다.",
"Options.OverlayCorner.Label": "화면 모서리",
"Options.OverlayCorner.Desc": "저장할 모서리를 선택합니다. F2는 이번 실행에서만 모서리를 바꿉니다. 제목 표시줄의 통계는 이동하지 않습니다.",
"Options.OverlayMode.Full": "전체",
"Options.OverlayMode.Minimal": "최소",
"Options.OverlayMode.TitleBar": "제목 표시줄 (최소)",
"Options.OverlayCorner.TopLeft": "왼쪽 위",
"Options.OverlayCorner.TopRight": "오른쪽 위",
"Options.OverlayCorner.BottomRight": "오른쪽 아래",
"Options.OverlayCorner.BottomLeft": "왼쪽 아래",
"Page.Library": "라이브러리",
"Page.Options": "옵션",
@ -69,8 +53,6 @@
"Options.TraceImports.Label": "가져오기 트레이스 한도",
"Options.TraceImports.Desc": "각 모듈의 처음 N개 가져오기를 트레이스합니다 (0 = 비활성화).",
"Options.CrashDump.Label": "크래시 덤프",
"Options.CrashDump.Desc": "실행 로그 옆에 크래시 덤프를 저장합니다(Windows x64 전용). 덤프는 크기가 클 수 있으며 개인 데이터를 포함할 수 있습니다.",
"Options.LogToFile.Label": "파일로 저장",
"Options.LogToFile.Desc": "에뮬레이터 출력을 로그 파일에 복사합니다.",
@ -108,17 +90,6 @@
"Launch.Idle": "대기 중",
"Launch.Console": "≡ 콘솔",
"Launch.Launch": "▶ 실행",
"Launch.CustomEnv": "사용자 지정 환경 변수로 실행",
"Launch.CustomEnv.Title": "사용자 지정 환경 변수로 실행",
"Launch.CustomEnv.Desc": "환경 변수를 NAME=VALUE 형식으로 한 줄에 하나씩 입력하세요. 사용자 지정 값은 이름이 같은 GUI 및 게임별 환경 설정을 덮어씁니다.",
"Launch.CustomEnv.Documentation": "문서화되지 않은 환경 변수 보기 ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_EXAMPLE=1",
"Launch.CustomEnv.SaveGame": "게임용으로 저장",
"Launch.CustomEnv.SaveGlobal": "전역으로 저장",
"Launch.CustomEnv.Play": "플레이",
"Launch.CustomEnv.Invalid": "잘못된 환경 변수: {0}",
"Launch.CustomEnv.SavedGame": "이 게임용으로 저장했습니다.",
"Launch.CustomEnv.SavedGlobal": "전역으로 저장했습니다.",
"Launch.Stop": "■ 중지",
"Launch.Running": "실행 중 — {0}",
"Launch.Stopping": "중지 중…",
@ -176,8 +147,9 @@
"Options.Env.LogNp.Desc": "NP(PlayStation Network) 라이브러리 호출을 콘솔에 기록합니다.",
"Options.Env.Group.Debug": "디버그",
"Options.Env.Group.General": "일반",
"Options.Env.RenderDoc.Desc": "RenderDoc의 인앱 API를 로드하여 에뮬레이터 내부에서 프레임을 캡처할 수 있게 합니다.\n게임 실행 중 F12을 누르면 한 프레임을 캡처하며, 캡처 파일은 user/logs/capture_logs/<TITLE_ID>에 저장됩니다.\nRenderDoc이 설치되어 있어야 합니다. GPU 속도가 느려지고 일부 타이틀은 멈추므로 디버깅할 때가 아니면 꺼 두세요.",
"Options.Env.DisableVkImplicitLayers.Desc": "RenderDoc이 활성화된 경우 다른 충돌하는 오버레이로 인해 device loss가 발생할 수 있습니다. 이 옵션을 활성화하면 문제가 해결될 수 있습니다.",
"Options.Env.RenderDoc.Desc": "RenderDoc의 인앱 API를 로드하여 에뮬레이터 내부에서 프레임을 캡처할 수 있게 합니다.\n게임 실행 중 F10을 누르면 한 프레임을 캡처하며, 캡처 파일은 user/logs/capture_logs/<TITLE_ID>에 저장됩니다.\nRenderDoc이 설치되어 있어야 합니다. GPU 속도가 느려지고 일부 타이틀은 멈추므로 디버깅할 때가 아니면 꺼 두세요.",
"Options.Env.GuestImageCpuSync.Desc": "게임의 자체 CPU 코드가 다시 쓰는 게스트 표면을 다시 업로드합니다.\n평소에는 꺼 두세요. CPU로 그린 표면이 화면에 나타나지 않는 타이틀에서 켜세요.\n성능을 소모하며 GTA V 등 일부 타이틀에서는 문제가 생깁니다.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "대상 큐가 절대 가져가지 않아도 GPU 명령 버퍼 프리앰블을 전달합니다.\n평소에는 꺼 두세요. 절대 신호를 보내지 않는 GPU 펜스를 기다리며 멈추는 타이틀에서 켜세요.",
"Common.Save": "저장",
"Common.Cancel": "취소",
"Options.About": "정보",
@ -188,7 +160,7 @@
"About.Discord.Label": "디스코드",
"About.Discord.Desc": "커뮤니티에 참여해 지원을 받고 개발 소식을 확인하세요.",
"About.GithubButton": "GitHub에서 기여하기!",
"About.DiscordButton": "Discord에 참여하세요!",
"About.DiscordComingSoon": "곧 공개",
"Updater.Auto.Label": "시작 시 업데이트 확인",
"Updater.Auto.Desc": "시작을 지연시키지 않고 GitHub를 확인합니다.",
"Updater.Label": "업데이트",
@ -205,13 +177,11 @@
"Updater.Status.ChecksumFailed": "다운로드한 업데이트가 SHA-256 검증에 실패했습니다.",
"Updater.Status.Unsupported": "자동 업데이트에는 Windows, Linux 또는 macOS x64 빌드가 필요합니다.",
"Options.Graphics": "그래픽",
"Options.Console.Type.Label": "콘솔",
"Options.Console.Type.Desc": "게임에 보고할 콘솔 모델입니다.",
"Options.Console.Language.Label": "콘솔 언어",
"Options.Console.Language.Desc": "게임에 보고할 시스템 언어입니다.",
"Options.Section.Rendering": "렌더링",
"Options.Section.Display": "디스플레이",
"Options.RenderResolution.Label": "내부 해상도",
"Options.RenderResolution.Desc": "네이티브 해상도보다 낮은 해상도로 오프스크린 대상을 렌더링한 뒤 표시할 때 업스케일합니다. 값이 낮을수록 화질을 희생해 GPU 여유를 확보하며 다음 실행부터 적용됩니다.",
"Options.RenderResolution.Native": "100% (네이티브)",
"Options.WindowMode.Label": "창 모드",
"Options.WindowMode.Desc": "일반 창, 데스크톱 테두리 없음 또는 독점 전체 화면.",
"Options.WindowMode.Windowed": "창",
@ -234,10 +204,5 @@
"Options.VSync.Desc": "티어링 없는 출력을 위해 FIFO 프레젠테이션을 사용합니다.",
"Options.Hdr.Label": "HDR 출력",
"Options.Hdr.Desc": "선택한 디스플레이와 그래픽 백엔드가 지원하는 경우 HDR을 사용합니다. 자동 모드는 SDR로 대체됩니다.",
"Options.Hdr.Auto": "자동",
"Options.Env.BinkPlayback.Label": "Bink 비디오 재생",
"Options.Env.BinkPlayback.Desc": "Host는 FFmpeg 브리지를 통해 Bink 비디오를 재생하지만 일부 게임에서 문제가 발생할 수 있습니다.\nGuest는 게임 엔진 자체 디코더를 사용합니다.\nSkip은 Bink 비디오를 건너뛰어 개발을 더 쉽게 할 수 있습니다.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "건너뛰기"
"Options.Hdr.Auto": "자동"
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "Nederlands",
"Options.Env.StrictCompute.Desc": "Stop bij shaderafwijzingen voor berekeningen en tekenen (standaard).\nSchakel uit om betrokken werk over te slaan. Ontbrekende beelden, fouten of vastlopers zijn mogelijk. Herstart het spel na een wijziging.",
"Options.Env.PerformanceProfile.Desc": "Log prestatietijden en FPS vanaf de volgende start van het spel.\nDit kan de prestaties verminderen.",
"Options.Env.PerformanceFrameTrace.Desc": "Bewaar de laatste 4.096 presentatie-intervallen en schrijf ze bij afsluiten naar het logboek.\nVereist prestatieprofilering. Sluit het spel normaal af om de trace op te slaan.",
"Options.OverlayEnabled.Label": "Prestatiegegevens",
"Options.OverlayEnabled.Desc": "Toon prestatiegegevens. F1 schakelt de weergave voor deze sessie in of uit. SHARPEMU_OVERLAY=0 verbergt deze bij het starten.",
"Options.OverlayMode.Label": "Weergavemodus",
"Options.OverlayMode.Desc": "Volledig toont alle tellers. Minimaal en Titelbalk tonen FPS, CPU, GPU-gebruik en verstreken tijd. GPU-gebruik geldt per proces op Windows; anders N/A.",
"Options.OverlayCorner.Label": "Schermhoek",
"Options.OverlayCorner.Desc": "Kies de opgeslagen hoek. F2 wisselt de hoek alleen voor deze sessie. Dit verplaatst de gegevens in de titelbalk niet.",
"Options.OverlayMode.Full": "Volledig",
"Options.OverlayMode.Minimal": "Minimaal",
"Options.OverlayMode.TitleBar": "Titelbalk (minimaal)",
"Options.OverlayCorner.TopLeft": "Linksboven",
"Options.OverlayCorner.TopRight": "Rechtsboven",
"Options.OverlayCorner.BottomRight": "Rechtsonder",
"Options.OverlayCorner.BottomLeft": "Linksonder",
"Page.Library": "Bibliotheek",
"Page.Options": "Opties",
@ -69,8 +53,6 @@
"Options.TraceImports.Label": "Tracelimiet voor imports",
"Options.TraceImports.Desc": "Traceer de eerste N imports per module (0 = uit).",
"Options.CrashDump.Label": "Crashdumps",
"Options.CrashDump.Desc": "Sla crashdumps naast het uitvoeringslog op (alleen Windows x64). Dumps kunnen groot zijn en privégegevens bevatten.",
"Options.LogToFile.Label": "Loggen naar bestand",
"Options.LogToFile.Desc": "Stuur de uitvoer van de emulator ook naar een logbestand.",
@ -108,17 +90,6 @@
"Launch.Idle": "Inactief",
"Launch.Console": "≡ Console",
"Launch.Launch": "▶ Starten",
"Launch.CustomEnv": "Starten met aangepaste omgevingsvariabelen",
"Launch.CustomEnv.Title": "Starten met aangepaste omgevingsvariabelen",
"Launch.CustomEnv.Desc": "Voer per regel één omgevingsvariabele in als NAAM=WAARDE. Aangepaste waarden overschrijven GUI- en spelspecifieke instellingen met dezelfde naam.",
"Launch.CustomEnv.Documentation": "Ongedocumenteerde omgevingsvariabelen bekijken ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_VOORBEELD=1",
"Launch.CustomEnv.SaveGame": "Opslaan voor spel",
"Launch.CustomEnv.SaveGlobal": "Globaal opslaan",
"Launch.CustomEnv.Play": "Spelen",
"Launch.CustomEnv.Invalid": "Ongeldige omgevingsvariabele: {0}",
"Launch.CustomEnv.SavedGame": "Opgeslagen voor dit spel.",
"Launch.CustomEnv.SavedGlobal": "Globaal opgeslagen.",
"Launch.Stop": "■ Stoppen",
"Launch.Running": "Actief — {0}",
"Launch.Stopping": "Stoppen…",
@ -176,8 +147,9 @@
"Options.Env.LogNp.Desc": "Log NP-bibliotheekaanroepen (PlayStation Network) naar de console.",
"Options.Env.Group.Debug": "Debuggen",
"Options.Env.Group.General": "Algemeen",
"Options.Env.RenderDoc.Desc": "Laadt de RenderDoc in-application-API zodat frames vanuit de emulator kunnen worden vastgelegd.\nDruk op F12 terwijl het spel draait om één frame vast te leggen; opnamen komen in user/logs/capture_logs/<TITLE_ID>.\nVereist een geïnstalleerde RenderDoc. Vertraagt de GPU en laat sommige games vastlopen, dus laat dit uit tenzij je aan het debuggen bent.",
"Options.Env.DisableVkImplicitLayers.Desc": "Wanneer RenderDoc is ingeschakeld, kunnen andere conflicterende overlays device loss veroorzaken. Het inschakelen van deze optie kan het probleem oplossen.",
"Options.Env.RenderDoc.Desc": "Laadt de RenderDoc in-application-API zodat frames vanuit de emulator kunnen worden vastgelegd.\nDruk op F10 terwijl het spel draait om één frame vast te leggen; opnamen komen in user/logs/capture_logs/<TITLE_ID>.\nVereist een geïnstalleerde RenderDoc. Vertraagt de GPU en laat sommige games vastlopen, dus laat dit uit tenzij je aan het debuggen bent.",
"Options.Env.GuestImageCpuSync.Desc": "Gastoppervlakken opnieuw uploaden die de eigen CPU-code van de game herschrijft.\nNormaal uit laten. Inschakelen voor titels waarvan de door de CPU getekende oppervlakken nooit het scherm bereiken.\nKost prestaties en veroorzaakt regressies in sommige titels, zoals GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Levert GPU-commandobuffer-preambules af, zelfs wanneer de doelwachtrij ze nooit ophaalt.\nNormaal uit laten. Inschakelen voor titels die vasthangen in afwachting van een GPU-fence die nooit signaleert.",
"Common.Save": "Opslaan",
"Common.Cancel": "Annuleren",
"Options.About": "Over",
@ -188,7 +160,7 @@
"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!",
"About.DiscordComingSoon": "Binnenkort",
"Updater.Auto.Label": "Bij het opstarten controleren op updates",
"Updater.Auto.Desc": "Controleert GitHub zonder het opstarten te vertragen.",
"Updater.Label": "Updates",
@ -205,13 +177,11 @@
"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.",
"Options.Graphics": "Grafisch",
"Options.Console.Type.Label": "Console",
"Options.Console.Type.Desc": "Consolemodel dat aan games wordt gemeld.",
"Options.Console.Language.Label": "Consoletaal",
"Options.Console.Language.Desc": "Systeemtaal die aan games wordt gemeld.",
"Options.Section.Rendering": "RENDERING",
"Options.Section.Display": "BEELDSCHERM",
"Options.RenderResolution.Label": "Interne resolutie",
"Options.RenderResolution.Desc": "Render offscreen-doelen onder de oorspronkelijke resolutie en schaal ze bij presentatie op. Lagere waarden ruilen beeldkwaliteit in voor GPU-marge; wordt bij de volgende start toegepast.",
"Options.RenderResolution.Native": "100% (native)",
"Options.WindowMode.Label": "Venstermodus",
"Options.WindowMode.Desc": "Normaal venster, randloos bureaublad of exclusief volledig scherm.",
"Options.WindowMode.Windowed": "Venster",
@ -234,10 +204,5 @@
"Options.VSync.Desc": "Gebruik FIFO-presentatie voor uitvoer zonder tearing.",
"Options.Hdr.Label": "HDR-uitvoer",
"Options.Hdr.Desc": "Gebruik HDR wanneer het geselecteerde beeldscherm en de grafische backend dit ondersteunen. Automatisch valt terug op SDR.",
"Options.Hdr.Auto": "Automatisch",
"Options.Env.BinkPlayback.Label": "Bink-video afspelen",
"Options.Env.BinkPlayback.Desc": "Host speelt Bink-video's af via de FFmpeg-brug, maar sommige games kunnen problemen hebben.\nGuest gebruikt de eigen decoder van de game-engine.\nSkip slaat Bink-video's over, wat de ontwikkeling kan vergemakkelijken.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Overslaan"
"Options.Hdr.Auto": "Automatisch"
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "Polski",
"Options.Env.StrictCompute.Desc": "Zatrzymuj przy odrzuceniu shaderów obliczeń i rysowania (domyślnie).\nWyłącz, aby pomijać te operacje. Mogą wystąpić brakujące obrazy, błędy lub zawieszenia. Po zmianie uruchom grę ponownie.",
"Options.Env.PerformanceProfile.Desc": "Zapisuj czasy wydajności i FPS od następnego uruchomienia gry.\nMoże to obniżyć wydajność.",
"Options.Env.PerformanceFrameTrace.Desc": "Zachowaj ostatnie 4096 odstępów prezentacji i zapisz je w dzienniku przy zamykaniu.\nWymaga profilowania wydajności. Zamknij grę normalnie, aby zapisać ślad.",
"Options.OverlayEnabled.Label": "Statystyki wydajności",
"Options.OverlayEnabled.Desc": "Pokaż statystyki. F1 włącza lub wyłącza widok tylko w tej sesji. SHARPEMU_OVERLAY=0 ukrywa go przy uruchomieniu.",
"Options.OverlayMode.Label": "Tryb wyświetlania",
"Options.OverlayMode.Desc": "Pełny pokazuje wszystkie liczniki. Minimalny i Pasek tytułu pokazują FPS, CPU, użycie GPU i czas. GPU jest mierzone dla procesu w Windows; w innych systemach N/A.",
"Options.OverlayCorner.Label": "Róg ekranu",
"Options.OverlayCorner.Desc": "Wybierz zapisany róg. F2 zmienia róg tylko w tej sesji. Nie przenosi statystyk z paska tytułu.",
"Options.OverlayMode.Full": "Pełny",
"Options.OverlayMode.Minimal": "Minimalny",
"Options.OverlayMode.TitleBar": "Pasek tytułu (minimalny)",
"Options.OverlayCorner.TopLeft": "Lewy górny",
"Options.OverlayCorner.TopRight": "Prawy górny",
"Options.OverlayCorner.BottomRight": "Prawy dolny",
"Options.OverlayCorner.BottomLeft": "Lewy dolny",
"Page.Library": "Biblioteka",
"Page.Options": "Opcje",
@ -62,8 +46,9 @@
"Options.Env.LogNp.Desc": "Loguj wywołania biblioteki NP (PlayStation Network) do konsoli.",
"Options.Env.Group.Debug": "Debugowanie",
"Options.Env.Group.General": "Ogólne",
"Options.Env.RenderDoc.Desc": "Załaduj API RenderDoc w aplikacji, aby można było przechwytywać klatki z wnętrza emulatora.\nNaciśnij F12 podczas działania gry, aby przechwycić jedną klatkę; przechwycone klatki trafiają do user/logs/capture_logs/<TITLE_ID>.\nWymaga zainstalowanego RenderDoc. Spowalnia GPU i zawiesza niektóre tytuły, więc zostaw wyłączone, chyba że debugujesz.",
"Options.Env.DisableVkImplicitLayers.Desc": "Gdy RenderDoc jest włączony, inne konfliktujące nakładki mogą powodować utratę urządzenia. Włączenie tej opcji może rozwiązać problem.",
"Options.Env.RenderDoc.Desc": "Załaduj API RenderDoc w aplikacji, aby można było przechwytywać klatki z wnętrza emulatora.\nNaciśnij F10 podczas działania gry, aby przechwycić jedną klatkę; przechwycone klatki trafiają do user/logs/capture_logs/<TITLE_ID>.\nWymaga zainstalowanego RenderDoc. Spowalnia GPU i zawiesza niektóre tytuły, więc zostaw wyłączone, chyba że debugujesz.",
"Options.Env.GuestImageCpuSync.Desc": "Ponownie przesyłaj powierzchnie gościa, które procesor gry nadpisuje.\nZostaw wyłączone normalnie. Włącz dla tytułów, których powierzchnie rysowane przez CPU nigdy nie docierają na ekran.\nKosztuje wydajność i pogarsza działanie niektórych tytułów, takich jak GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Dostarczaj preambuły buforów poleceń GPU nawet gdy ich docelowa kolejka ich nie odbiera.\nZostaw wyłączone normalnie. Włącz dla tytułów, które zawieszają się w oczekiwaniu na sygnał z GPU.",
"Options.DefaultProfile.Label": "Domyślna nazwa profilu",
"Options.DefaultProfile.Desc": "Nazwa używana, gdy gra prosi o wprowadzenie tekstu. Domyślnie Sharp.",
"Options.Section.Emulation": "EMULACJA",
@ -73,11 +58,9 @@
"Options.Section.Display": "WYŚWIETLANIE",
"Options.Graphics": "Grafika",
"Options.Console.Type.Label": "Konsola",
"Options.Console.Type.Desc": "Model konsoli zgłaszany grom.",
"Options.Console.Language.Label": "Język konsoli",
"Options.Console.Language.Desc": "Język systemu zgłaszany grom.",
"Options.RenderResolution.Label": "Rozdzielczość wewnętrzna",
"Options.RenderResolution.Desc": "Renderuj cele pozaekranowe poniżej natywnej rozdzielczości i skaluj w górę przy prezentacji. Niższe wartości zamieniają jakość obrazu na zapas GPU; działa od następnego uruchomienia.",
"Options.RenderResolution.Native": "100% (natywna)",
"Options.WindowMode.Label": "Tryb okna",
"Options.WindowMode.Desc": "Zwykłe okno, bezramkowe na pulpicie lub pełny ekran wyłączny.",
"Options.WindowMode.Windowed": "W oknie",
@ -101,11 +84,6 @@
"Options.Hdr.Label": "Wyjście HDR",
"Options.Hdr.Desc": "Używaj HDR, gdy wybrany wyświetlacz i backend graficzny to obsługują. Automatyczny przełącza na SDR.",
"Options.Hdr.Auto": "Automatyczny",
"Options.Env.BinkPlayback.Label": "Odtwarzanie wideo Bink",
"Options.Env.BinkPlayback.Desc": "Host odtwarza filmy Bink przez most FFmpeg, ale w niektórych grach mogą wystąpić problemy.\nGuest używa dekodera silnika gry.\nSkip pomija filmy Bink, co może ułatwić rozwój.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Pomiń",
"Options.CpuEngine.Label": "Silnik CPU",
"Options.CpuEngine.Desc": "Silnik wykonawczy używany do uruchamiania kodu gry.",
@ -126,8 +104,6 @@
"Options.TraceImports.Label": "Limit śledzenia importów",
"Options.TraceImports.Desc": "Śledź pierwszych N importów na moduł (0 = wyłączone).",
"Options.CrashDump.Label": "Zrzuty pamięci po awarii",
"Options.CrashDump.Desc": "Zapisuj zrzuty pamięci obok dziennika uruchomienia (tylko Windows x64). Pliki mogą być duże i zawierać prywatne dane.",
"Options.LogToFile.Label": "Loguj do pliku",
"Options.LogToFile.Desc": "Kopiuj wyjście emulatora do pliku logu.",
@ -166,17 +142,6 @@
"Launch.Idle": "Bezczynny",
"Launch.Console": "≡ Konsola",
"Launch.Launch": "▶ Uruchom",
"Launch.CustomEnv": "Uruchom z własnymi zmiennymi środowiskowymi",
"Launch.CustomEnv.Title": "Uruchom z własnymi zmiennymi środowiskowymi",
"Launch.CustomEnv.Desc": "Wpisz po jednej zmiennej środowiskowej w wierszu w formacie NAZWA=WARTOŚĆ. Własne wartości zastępują ustawienia GUI i ustawienia gry o tej samej nazwie.",
"Launch.CustomEnv.Documentation": "Wyświetl nieudokumentowane zmienne środowiskowe ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_PRZYKLAD=1",
"Launch.CustomEnv.SaveGame": "Zapisz dla gry",
"Launch.CustomEnv.SaveGlobal": "Zapisz globalnie",
"Launch.CustomEnv.Play": "Graj",
"Launch.CustomEnv.Invalid": "Nieprawidłowa zmienna środowiskowa: {0}",
"Launch.CustomEnv.SavedGame": "Zapisano dla tej gry.",
"Launch.CustomEnv.SavedGlobal": "Zapisano globalnie.",
"Launch.Stop": "■ Zatrzymaj",
"Launch.Running": "Działa — {0}",
"Launch.Stopping": "Zatrzymywanie…",
@ -228,7 +193,7 @@
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Dołącz do społeczności, uzyskaj wsparcie i śledź rozwój.",
"About.GithubButton": "Wspieraj na GitHubie!",
"About.DiscordButton": "Dołącz do naszego Discorda!",
"About.DiscordComingSoon": "Wkrótce",
"Updater.Auto.Label": "Sprawdzaj aktualizacje przy uruchomieniu",
"Updater.Auto.Desc": "Sprawdza GitHub bez opóźniania uruchomienia.",

View File

@ -1,21 +1,5 @@
{
"_languageName": "Português (Portugal)",
"Options.Env.StrictCompute.Desc": "Parar nas rejeições de shaders de cálculo e desenho (predefinição).\nDesativa para ignorar o trabalho afetado. Podem ocorrer imagens em falta, erros ou bloqueios. Reinicia o jogo após uma alteração.",
"Options.Env.PerformanceProfile.Desc": "Regista os tempos de desempenho e FPS a partir do próximo arranque do jogo.\nIsto pode reduzir o desempenho.",
"Options.Env.PerformanceFrameTrace.Desc": "Guarda os últimos 4096 intervalos de apresentação e escreve-os no registo ao sair.\nRequer o perfil de desempenho. Fecha o jogo normalmente para guardar o rastreio.",
"Options.OverlayEnabled.Label": "Indicador de desempenho",
"Options.OverlayEnabled.Desc": "Mostrar estatísticas. F1 ativa ou oculta o indicador nesta sessão. SHARPEMU_OVERLAY=0 oculta-o no arranque.",
"Options.OverlayMode.Label": "Modo de apresentação",
"Options.OverlayMode.Desc": "Completo mostra todos os contadores. Mínimo e Barra de título mostram FPS, CPU, utilização da GPU e tempo. A GPU é medida por processo no Windows; nos restantes sistemas, N/A.",
"Options.OverlayCorner.Label": "Canto do ecrã",
"Options.OverlayCorner.Desc": "Escolher o canto guardado. F2 muda de canto apenas nesta sessão. Não move as estatísticas da barra de título.",
"Options.OverlayMode.Full": "Completo",
"Options.OverlayMode.Minimal": "Mínimo",
"Options.OverlayMode.TitleBar": "Barra de título (mínimo)",
"Options.OverlayCorner.TopLeft": "Superior esquerdo",
"Options.OverlayCorner.TopRight": "Superior direito",
"Options.OverlayCorner.BottomRight": "Inferior direito",
"Options.OverlayCorner.BottomLeft": "Inferior esquerdo",
"Page.Library": "Biblioteca",
"Page.Options": "Opções",
@ -57,8 +41,9 @@
"Options.Env.LogNp.Desc": "Regista chamadas da biblioteca NP (PlayStation Network) na consola.",
"Options.Env.Group.Debug": "Depuração",
"Options.Env.Group.General": "Geral",
"Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames a partir do emulador.\nPrime F12 enquanto o jogo corre para capturar um frame; as capturas vão para user/logs/capture_logs/<TITLE_ID>.\nRequer o RenderDoc instalado. Torna a GPU mais lenta e bloqueia alguns títulos, por isso deixa desativado a menos que estejas a depurar.",
"Options.Env.DisableVkImplicitLayers.Desc": "Quando o RenderDoc está ativado, outros overlays em conflito podem causar perda do dispositivo. Ativar esta opção pode corrigir o problema.",
"Options.Env.RenderDoc.Desc": "Carrega a API in-application do RenderDoc para capturar frames a partir do emulador.\nPrime F10 enquanto o jogo corre para capturar um frame; as capturas vão para user/logs/capture_logs/<TITLE_ID>.\nRequer o RenderDoc instalado. Torna a GPU mais lenta e bloqueia alguns títulos, por isso deixa desativado a menos que estejas a depurar.",
"Options.Env.GuestImageCpuSync.Desc": "Recarrega as superfícies do convidado que o próprio código de CPU do jogo reescreve.\nDeixar desativado normalmente. Ativar para títulos cujas superfícies desenhadas pela CPU nunca chegam ao ecrã.\nCusta desempenho e causa regressões em alguns títulos, como GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Entrega os preâmbulos do buffer de comandos da GPU mesmo quando a fila de destino nunca os recolhe.\nDeixar desativado normalmente. Ativar para títulos que bloqueiam à espera de uma fence de GPU que nunca sinaliza.",
"Options.Section.Emulation": "EMULAÇÃO",
"Options.Section.Logging": "REGISTOS",
"Options.Section.Launcher": "LANÇADOR",
@ -82,8 +67,6 @@
"Options.TraceImports.Label": "Limite de rastreio de importações",
"Options.TraceImports.Desc": "Rastreia as primeiras N importações por módulo (0 = desativado).",
"Options.CrashDump.Label": "Ficheiros de memória de falhas",
"Options.CrashDump.Desc": "Guarda os ficheiros de memória de falhas junto ao registo de execução (apenas Windows x64). Os ficheiros podem ser grandes e conter dados privados.",
"Options.LogToFile.Label": "Registar para ficheiro",
"Options.LogToFile.Desc": "Duplicar a saída do emulador para um ficheiro de registo.",
@ -121,17 +104,6 @@
"Launch.Idle": "Inativo",
"Launch.Console": "≡ Consola",
"Launch.Launch": "▶ Iniciar",
"Launch.CustomEnv": "Iniciar com variáveis de ambiente personalizadas",
"Launch.CustomEnv.Title": "Iniciar com variáveis de ambiente personalizadas",
"Launch.CustomEnv.Desc": "Introduza uma variável de ambiente por linha no formato NOME=VALOR. Os valores personalizados substituem as definições da GUI e específicas do jogo com o mesmo nome.",
"Launch.CustomEnv.Documentation": "Ver variáveis de ambiente não documentadas ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_EXEMPLO=1",
"Launch.CustomEnv.SaveGame": "Guardar para o jogo",
"Launch.CustomEnv.SaveGlobal": "Guardar globalmente",
"Launch.CustomEnv.Play": "Jogar",
"Launch.CustomEnv.Invalid": "Variável de ambiente inválida: {0}",
"Launch.CustomEnv.SavedGame": "Guardado para este jogo.",
"Launch.CustomEnv.SavedGlobal": "Guardado globalmente.",
"Launch.Stop": "■ Parar",
"Launch.Running": "Em execução — {0}",
"Launch.Stopping": "A parar…",
@ -181,7 +153,7 @@
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Junte-se à comunidade, obtenha suporte e acompanhe o desenvolvimento.",
"About.GithubButton": "Contribua no GitHub!",
"About.DiscordButton": "Junte-se ao nosso Discord!",
"About.DiscordComingSoon": "Em breve",
"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.",
@ -206,13 +178,11 @@
"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.",
"Options.Graphics": "Gráficos",
"Options.Console.Type.Label": "Consola",
"Options.Console.Type.Desc": "Modelo de consola comunicado aos jogos.",
"Options.Console.Language.Label": "Idioma da consola",
"Options.Console.Language.Desc": "Idioma do sistema comunicado aos jogos.",
"Options.Section.Rendering": "RENDERIZAÇÃO",
"Options.Section.Display": "ECRÃ",
"Options.RenderResolution.Label": "Resolução interna",
"Options.RenderResolution.Desc": "Renderiza alvos fora do ecrã abaixo da resolução nativa e amplia-os na apresentação. Valores inferiores trocam qualidade de imagem por margem da GPU; entra em vigor no próximo arranque.",
"Options.RenderResolution.Native": "100% (nativa)",
"Options.WindowMode.Label": "Modo de janela",
"Options.WindowMode.Desc": "Janela normal, ambiente de trabalho sem margens ou ecrã inteiro exclusivo.",
"Options.WindowMode.Windowed": "Em janela",
@ -235,10 +205,5 @@
"Options.VSync.Desc": "Utiliza apresentação FIFO para evitar cortes na imagem.",
"Options.Hdr.Label": "Saída HDR",
"Options.Hdr.Desc": "Utiliza HDR quando o ecrã selecionado e o backend gráfico o suportam. O modo automático regressa a SDR.",
"Options.Hdr.Auto": "Automático",
"Options.Env.BinkPlayback.Label": "Reprodução de vídeo Bink",
"Options.Env.BinkPlayback.Desc": "Host reproduz vídeos Bink pela ponte FFmpeg, mas alguns jogos podem apresentar problemas.\nGuest usa o descodificador do próprio motor do jogo.\nSkip ignora os vídeos Bink, o que pode facilitar o desenvolvimento.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Ignorar"
"Options.Hdr.Auto": "Automático"
}

View File

@ -1,21 +1,5 @@
{
"_languageName": "Русский",
"Options.Env.StrictCompute.Desc": "Останавливать при отказах шейдеров вычисления и отрисовки (по умолчанию).\nОтключите, чтобы пропускать эти операции. Возможны пропавшие изображения, ошибки и зависания. После изменения перезапустите игру.",
"Options.Env.PerformanceProfile.Desc": "Записывать время выполнения операций и FPS со следующего запуска игры.\nЭто может снизить производительность.",
"Options.Env.PerformanceFrameTrace.Desc": "Сохранять последние 4096 интервалов показа кадров и записывать их в журнал при завершении.\nТребуется профилирование производительности. Закройте игру обычным способом, чтобы сохранить трассировку.",
"Options.OverlayEnabled.Label": "Показатели производительности",
"Options.OverlayEnabled.Desc": "Показывать статистику. F1 переключает показ только в текущем сеансе. SHARPEMU_OVERLAY=0 скрывает его при запуске.",
"Options.OverlayMode.Label": "Режим отображения",
"Options.OverlayMode.Desc": "Полный режим показывает все счётчики. Минимальный и Строка заголовка показывают FPS, CPU, загрузку GPU и время. GPU измеряется для процесса в Windows; иначе N/A.",
"Options.OverlayCorner.Label": "Угол экрана",
"Options.OverlayCorner.Desc": "Выберите сохраняемый угол. F2 меняет угол только в текущем сеансе. Статистика в строке заголовка не перемещается.",
"Options.OverlayMode.Full": "Полный",
"Options.OverlayMode.Minimal": "Минимальный",
"Options.OverlayMode.TitleBar": "Строка заголовка (минимум)",
"Options.OverlayCorner.TopLeft": "Слева вверху",
"Options.OverlayCorner.TopRight": "Справа вверху",
"Options.OverlayCorner.BottomRight": "Справа внизу",
"Options.OverlayCorner.BottomLeft": "Слева внизу",
"Page.Library": "Библиотека",
"Page.Options": "Настройки",
@ -60,8 +44,9 @@
"Options.Env.LogNp.Desc": "Выводить в консоль вызовы библиотеки NP (PlayStation Network).",
"Options.Env.Group.Debug": "Отладка",
"Options.Env.Group.General": "Общие",
"Options.Env.RenderDoc.Desc": "Загружает внутренний API RenderDoc, чтобы кадры можно было захватывать из эмулятора.\nНажмите F12 во время игры, чтобы захватить один кадр; захваты сохраняются в user/logs/capture_logs/<TITLE_ID>.\nТребует установленного RenderDoc. Замедляет GPU и подвешивает некоторые игры, поэтому оставьте выключенным, если не занимаетесь отладкой.",
"Options.Env.DisableVkImplicitLayers.Desc": "Когда RenderDoc включён, другие конфликтующие оверлеи могут вызвать потерю устройства. Включение этой настройки может решить проблему.",
"Options.Env.RenderDoc.Desc": "Загружает внутренний API RenderDoc, чтобы кадры можно было захватывать из эмулятора.\nНажмите F10 во время игры, чтобы захватить один кадр; захваты сохраняются в user/logs/capture_logs/<TITLE_ID>.\nТребует установленного RenderDoc. Замедляет GPU и подвешивает некоторые игры, поэтому оставьте выключенным, если не занимаетесь отладкой.",
"Options.Env.GuestImageCpuSync.Desc": "Повторно загружать гостевые поверхности, которые переписывает собственный код ЦП игры.\nОбычно оставляйте выключенным. Включайте для игр, чьи отрисованные ЦП поверхности не попадают на экран.\nСнижает производительность и вызывает регрессии в некоторых играх, например в GTA V.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Доставляет преамбулы буфера команд GPU, даже если целевая очередь их так и не забирает.\nОбычно оставляйте выключенным. Включайте для игр, которые зависают в ожидании GPU-fence, который никогда не срабатывает.",
"Options.Section.Emulation": "ЭМУЛЯЦИЯ",
"Options.Section.Logging": "ЛОГГИРОВАНИЕ",
"Options.Section.Launcher": "ЛАУНЧЕР",
@ -69,11 +54,9 @@
"Options.Section.Display": "ЭКРАН",
"Options.Graphics": "Графика",
"Options.Console.Type.Label": "Консоль",
"Options.Console.Type.Desc": "Модель консоли, сообщаемая играм.",
"Options.Console.Language.Label": "Язык консоли",
"Options.Console.Language.Desc": "Системный язык, сообщаемый играм.",
"Options.RenderResolution.Label": "Внутреннее разрешение",
"Options.RenderResolution.Desc": "Рендерить внеэкранные буферы ниже нативного разрешения и масштабировать при выводе. Меньшие значения снижают качество изображения, но уменьшают нагрузку на GPU; применяется при следующем запуске.",
"Options.RenderResolution.Native": "100% (нативное)",
"Options.WindowMode.Label": "Режим окна",
"Options.WindowMode.Desc": "Обычное окно, безрамочный режим рабочего стола или эксклюзивный полноэкранный режим.",
"Options.WindowMode.Windowed": "Оконный",
@ -97,11 +80,6 @@
"Options.Hdr.Label": "HDR-вывод",
"Options.Hdr.Desc": "Использовать HDR, если выбранный монитор и графический бэкенд его поддерживают. Автоматический режим при необходимости переключается на SDR.",
"Options.Hdr.Auto": "Авто",
"Options.Env.BinkPlayback.Label": "Воспроизведение видео Bink",
"Options.Env.BinkPlayback.Desc": "Host воспроизводит видео Bink через мост FFmpeg, однако в некоторых играх могут возникать проблемы.\nGuest использует собственный декодер игрового движка.\nSkip пропускает видео Bink, что может упростить разработку.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Пропустить",
"Options.CpuEngine.Label": "Движок ЦП",
"Options.CpuEngine.Desc": "Движок выполнения, используемый для запуска игрового кода.",
@ -122,8 +100,6 @@
"Options.TraceImports.Label": "Лимит трассировки импортов",
"Options.TraceImports.Desc": "Трассировать первые N импортов в каждом модуле (0 - выключено).",
"Options.CrashDump.Label": "Дампы сбоев",
"Options.CrashDump.Desc": "Сохранять дампы сбоев рядом с журналом запуска (только Windows x64). Файлы могут быть большими и содержать личные данные.",
"Options.LogToFile.Label": "Запись лога в файл",
"Options.LogToFile.Desc": "Дублировать вывод эмулятора в файл лога.",
@ -164,17 +140,6 @@
"Launch.Idle": "Ожидание",
"Launch.Console": "≡ Консоль",
"Launch.Launch": "▶ Запустить",
"Launch.CustomEnv": "Запустить с пользовательскими переменными среды",
"Launch.CustomEnv.Title": "Запустить с пользовательскими переменными среды",
"Launch.CustomEnv.Desc": "Введите по одной переменной среды на строку в формате ИМЯ=ЗНАЧЕНИЕ. Пользовательские значения переопределяют одноимённые настройки GUI и игры.",
"Launch.CustomEnv.Documentation": "Показать недокументированные переменные среды ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_PRIMER=1",
"Launch.CustomEnv.SaveGame": "Сохранить для игры",
"Launch.CustomEnv.SaveGlobal": "Сохранить глобально",
"Launch.CustomEnv.Play": "Играть",
"Launch.CustomEnv.Invalid": "Недопустимая переменная среды: {0}",
"Launch.CustomEnv.SavedGame": "Сохранено для этой игры.",
"Launch.CustomEnv.SavedGlobal": "Сохранено глобально.",
"Launch.Stop": "■ Остановить",
"Launch.Running": "Запущено - {0}",
"Launch.Stopping": "Остановка…",
@ -226,7 +191,7 @@
"About.Discord.Label": "Discord",
"About.Discord.Desc": "Присоединяйтесь к сообществу, получайте поддержку и следите за разработкой.",
"About.GithubButton": "Участвовать в разработке на GitHub!",
"About.DiscordButton": "Присоединяйтесь к нашему Discord!",
"About.DiscordComingSoon": "Скоро",
"Updater.Auto.Label": "Проверять обновления при запуске",
"Updater.Auto.Desc": "Проверяет GitHub без задержки запуска.",

View File

@ -1,21 +1,5 @@
{
"_languageName": "Türkçe",
"Options.Env.StrictCompute.Desc": "Hesaplama ve çizim gölgelendiricisi retlerinde durdur (varsayılan).\nEtkilenen işlemleri atlamak için kapat. Eksik görüntüler, hatalar veya takılmalar olabilir. Değişiklikten sonra oyunu yeniden başlatın.",
"Options.Env.PerformanceProfile.Desc": "Sonraki oyun başlangıcından itibaren performans sürelerini ve FPS değerlerini kaydet.\nBu, performansı düşürebilir.",
"Options.Env.PerformanceFrameTrace.Desc": "Son 4.096 sunum aralığını sakla ve kapanışta günlüğe yaz.\nPerformans profillemesi gerektirir. İzi kaydetmek için oyunu normal şekilde kapat.",
"Options.OverlayEnabled.Label": "Performans göstergesi",
"Options.OverlayEnabled.Desc": "Performans istatistiklerini gösterir. F1 göstergeyi bu oturum için açar veya kapatır. SHARPEMU_OVERLAY=0 başlangıçta gizler.",
"Options.OverlayMode.Label": "Görüntüleme modu",
"Options.OverlayMode.Desc": "Tam tüm sayaçları gösterir. Asgari ve Başlık çubuğu FPS, CPU, GPU kullanımı ve geçen süreyi gösterir. GPU kullanımı Windows’ta işlem bazındadır; diğer sistemlerde N/A gösterilir.",
"Options.OverlayCorner.Label": "Ekran köşesi",
"Options.OverlayCorner.Desc": "Kaydedilecek köşeyi seçin. F2 köşeyi yalnızca bu oturum için değiştirir. Başlık çubuğundaki istatistikleri taşımaz.",
"Options.OverlayMode.Full": "Tam",
"Options.OverlayMode.Minimal": "Asgari",
"Options.OverlayMode.TitleBar": "Başlık çubuğu (asgari)",
"Options.OverlayCorner.TopLeft": "Sol üst",
"Options.OverlayCorner.TopRight": "Sağ üst",
"Options.OverlayCorner.BottomRight": "Sağ alt",
"Options.OverlayCorner.BottomLeft": "Sol alt",
"Page.Library": "Kütüphane",
"Page.Options": "Ayarlar",
@ -54,11 +38,6 @@
"Options.Section.Display": "GÖRÜNTÜ",
"Options.Graphics": "Grafik",
"Options.Console.Type.Label": "Konsol",
"Options.Console.Type.Desc": "Oyunlara bildirilen konsol modeli.",
"Options.Console.Language.Label": "Konsol dili",
"Options.Console.Language.Desc": "Oyunlara bildirilen sistem dili.",
"Options.WindowMode.Label": "Pencere modu",
"Options.WindowMode.Desc": "Normal pencere, kenarlıksız masaüstü veya özel tam ekran.",
"Options.Resolution.Label": "Çözünürlük",
@ -94,8 +73,6 @@
"Options.TraceImports.Label": "Import izleme sınırı",
"Options.TraceImports.Desc": "Modül başına ilk N import'u izle (0 = kapalı).",
"Options.CrashDump.Label": "Çökme dökümleri",
"Options.CrashDump.Desc": "Çökme dökümlerini çalıştırma günlüğünün yanına kaydeder (yalnızca Windows x64). Dökümler büyük olabilir ve özel veriler içerebilir.",
"Options.LogToFile.Label": "Dosyaya günlükle",
"Options.LogToFile.Desc": "Emülatör çıktısını bir günlük dosyasına yansıt.",
@ -131,17 +108,6 @@
"Launch.Idle": "Boşta",
"Launch.Console": "≡ Konsol",
"Launch.Launch": "▶ Başlat",
"Launch.CustomEnv": "Özel env ile başlat",
"Launch.CustomEnv.Title": "Özel env ile başlat",
"Launch.CustomEnv.Desc": "Ortam değişkenlerini NAME=VALUE biçiminde her satıra bir tane yazın. Özel değerler, aynı isimdeki GUI ve oyuna özel ortam ayarlarını geçersiz kılar.",
"Launch.CustomEnv.Documentation": "Belgelenmemiş ortam değişkenlerini görüntüle ↗",
"Launch.CustomEnv.Placeholder": "SHARPEMU_ORNEK=1",
"Launch.CustomEnv.SaveGame": "Oyun için kaydet",
"Launch.CustomEnv.SaveGlobal": "Genel olarak kaydet",
"Launch.CustomEnv.Play": "Oyna",
"Launch.CustomEnv.Invalid": "Geçersiz ortam değişkeni: {0}",
"Launch.CustomEnv.SavedGame": "Bu oyun için kaydedildi.",
"Launch.CustomEnv.SavedGlobal": "Genel olarak kaydedildi.",
"Launch.Stop": "■ Durdur",
"Launch.Running": "Çalışıyor — {0}",
"Launch.Stopping": "Durduruluyor…",
@ -215,8 +181,9 @@
"Options.Env.LogNp.Desc": "NP (PlayStation Network) kütüphane çağrılarını konsola günlükle.",
"Options.Env.Group.Debug": "Hata Ayıklama",
"Options.Env.Group.General": "Genel",
"Options.Env.RenderDoc.Desc": "RenderDoc uygulama içi API'sini yükler, böylece kareler emülatörün içinden yakalanabilir.\nOyun çalışırken F12'a basınca bir kare yakalanır; kayıtlar user/logs/capture_logs/<TITLE_ID> altına düşer.\nRenderDoc'un kurulu olmasını gerektirir. GPU'yu yavaşlatır ve bazı oyunları kilitler, hata ayıklamıyorsanız kapalı bırakın.",
"Options.Env.DisableVkImplicitLayers.Desc": "RenderDoc ayarı açıkken diğer çakışan overlayler device loss'a sebebiyet verebiliyor. Bu ayarı açmak bunu düzeltebilir.",
"Options.Env.RenderDoc.Desc": "RenderDoc uygulama içi API'sini yükler, böylece kareler emülatörün içinden yakalanabilir.\nOyun çalışırken F10'a basınca bir kare yakalanır; kayıtlar user/logs/capture_logs/<TITLE_ID> altına düşer.\nRenderDoc'un kurulu olmasını gerektirir. GPU'yu yavaşlatır ve bazı oyunları kilitler, hata ayıklamıyorsanız kapalı bırakın.",
"Options.Env.GuestImageCpuSync.Desc": "Oyunun kendi CPU kodunun yeniden yazdığı misafir yüzeyleri tekrar yükler.\nNormalde kapalı bırakın. CPU ile çizilen yüzeyleri ekrana ulaşmayan oyunlarda açın.\nPerformansa mal olur ve GTA V gibi bazı oyunlarda soruna yol açar.",
"Options.Env.ForceSubmitOrphanPreambles.Desc": "Hedef kuyruk onları asla almasa bile GPU komut arabelleği önsözlerini teslim eder.\nNormalde kapalı bırakın. Asla sinyal vermeyen bir GPU fence'ini bekleyerek takılan oyunlarda açın.",
"Options.DefaultProfile.Label": "Varsayilan profil adi",
"Options.DefaultProfile.Desc": "Oyun metin girisi istediginde kullanilacak ad. Varsayilan deger Sharp'tir.",
"Common.Save": "Kaydet",
@ -229,8 +196,11 @@
"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'a katıl!",
"About.DiscordComingSoon": "Yakında",
"Options.Section.Rendering": "GÖRÜNTÜ İŞLEME",
"Options.RenderResolution.Label": "Dahili çözünürlük",
"Options.RenderResolution.Desc": "Ekran dışı hedefleri doğal çözünürlüğün altında işle ve sunum sırasında ölçeklendir. Daha düşük değerler GPU payı karşılığında görüntü kalitesini azaltır; bir sonraki başlatmada etkili olur.",
"Options.RenderResolution.Native": "%100 (doğal)",
"Options.WindowMode.Windowed": "Pencereli",
"Options.WindowMode.Borderless": "Kenarlıksız",
"Options.WindowMode.Exclusive": "Özel",
@ -238,10 +208,5 @@
"Options.Scaling.Cover": "Kapla",
"Options.Scaling.Stretch": "Uzat",
"Options.Scaling.Integer": "Tam sayı",
"Options.Hdr.Auto": "Otomatik",
"Options.Env.BinkPlayback.Label": "Bink video oynatma",
"Options.Env.BinkPlayback.Desc": "Host, Bink videolarını FFmpeg köprüsüyle oynatır ancak bazı oyunlarda sorunlarla karşılaşabilir.\nGuest, oyun motorunun kendi kod çözücüsünü kullanır.\nSkip, Bink videolarını atlar; bu, geliştirmeyi kolaylaştırabilir.",
"Options.Env.BinkPlayback.Host": "Host (FFmpeg)",
"Options.Env.BinkPlayback.Guest": "Guest",
"Options.Env.BinkPlayback.Skip": "Atla"
"Options.Hdr.Auto": "Otomatik"
}

View File

@ -1,37 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.GUI;
internal readonly record struct LibraryTileMetrics(
double CoverSize,
double ItemWidth,
double RailItemHeight,
double GridItemHeight);
internal static class LibraryLayoutMetrics
{
internal const double MinimumCoverSize = 112;
internal const double MaximumCoverSize = 148;
private const double HorizontalPadding = 24;
private const double ItemWidthExtra = 12;
private const double ItemMargin = 12;
private const double RailHeightExtra = 24;
private const double GridHeightExtra = 52;
private const double TargetColumnCount = 9;
internal static LibraryTileMetrics Calculate(double availableWidth)
{
var contentWidth = Math.Max(0, availableWidth - HorizontalPadding);
var coverSize = Math.Clamp(
(contentWidth / TargetColumnCount) - ItemWidthExtra - ItemMargin,
MinimumCoverSize,
MaximumCoverSize);
return new LibraryTileMetrics(
CoverSize: coverSize,
ItemWidth: coverSize + ItemWidthExtra,
RailItemHeight: coverSize + RailHeightExtra,
GridItemHeight: coverSize + GridHeightExtra);
}
}

View File

@ -13,9 +13,6 @@ public partial class MainWindow
{
private static readonly string[] GameEnvironmentToggleNames =
[
"SHARPEMU_PROFILE_PERFORMANCE",
"SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE",
StrictComputeSettings.VariableName,
"SHARPEMU_BTHID_UNAVAILABLE",
"SHARPEMU_DISABLE_IMPORT_LOOP_GUARD",
"SHARPEMU_WRITABLE_APP0",
@ -24,9 +21,8 @@ public partial class MainWindow
"SHARPEMU_LOG_DIRECT_MEMORY",
"SHARPEMU_LOG_IO",
"SHARPEMU_LOG_NP",
"SHARPEMU_GUEST_IMAGE_CPU_SYNC",
"SHARPEMU_RENDERDOC",
"SHARPEMU_VK_DISABLE_IMPLICITS",
"SHARPEMU_CRASH_CAPTURE",
];
private readonly List<string> _gameEnvironmentPassthrough = new();
@ -46,8 +42,6 @@ public partial class MainWindow
GameWindowModeBox.ItemsSource = _windowModeChoices;
GameScalingModeBox.ItemsSource = _scalingModeChoices;
GameHdrModeBox.ItemsSource = _hdrModeChoices;
GameOverlayModeBox.ItemsSource = _overlayModeChoices;
GameOverlayCornerBox.ItemsSource = _overlayCornerChoices;
var navigationButtons = GameOptionsNavigationButtons();
for (var index = 0; index < navigationButtons.Length; index++)
@ -116,9 +110,6 @@ public partial class MainWindow
GameScalingModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
GameVSyncToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
GameHdrModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
GameOverlayEnabledToggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
GameOverlayModeBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
GameOverlayCornerBox.SelectionChanged += (_, _) => PersistOpenGameSettings();
foreach (var (_, toggle) in GameEnvironmentToggles())
{
toggle.IsCheckedChanged += (_, _) => PersistOpenGameSettings();
@ -151,7 +142,6 @@ public partial class MainWindow
!string.IsNullOrWhiteSpace(game.TitleId);
_isGameSettingsOpen = true;
UpdateEmbeddedConsoleVisibility();
SetGameOptionsPagesSpan(coversConsoleRow: true);
SetGameOptionsOpenClass(BackdropLayer, active: true);
SetGameOptionsOpenClass(CarouselHost, active: true);
@ -171,7 +161,6 @@ public partial class MainWindow
}
_isGameSettingsOpen = false;
UpdateEmbeddedConsoleVisibility();
SetGameOptionsPagesSpan(coversConsoleRow: false);
SetGameOptionsNavigationIndicator(_gameOptionsIndicatorIndex, animate: false);
_gameSettingsTitleId = null;
@ -217,9 +206,6 @@ public partial class MainWindow
effective.ScalingMode,
"Fit");
GameVSyncToggle.IsChecked = effective.VSync;
GameOverlayEnabledToggle.IsChecked = effective.OverlayEnabled;
GameOverlayModeBox.SelectedItem = FindChoice(_overlayModeChoices, effective.OverlayMode, "TitleBar");
GameOverlayCornerBox.SelectedItem = FindChoice(_overlayCornerChoices, effective.OverlayCorner, "TopRight");
GameHdrModeBox.SelectedItem = FindChoice(
_hdrModeChoices,
effective.HdrMode,
@ -249,7 +235,9 @@ public partial class MainWindow
foreach (var (name, toggle) in GameEnvironmentToggles())
{
toggle.IsChecked = IsEnvironmentEnabled(effective.EnvironmentToggles, name);
toggle.IsChecked = IsEnvironmentEnabled(
effective.EnvironmentToggles,
name);
}
}
finally
@ -284,11 +272,7 @@ public partial class MainWindow
ScalingMode = SelectedComboText(GameScalingModeBox, "Fit"),
VSync = GameVSyncToggle.IsChecked == true,
HdrMode = SelectedComboText(GameHdrModeBox, "Auto"),
OverlayEnabled = GameOverlayEnabledToggle.IsChecked == true,
OverlayMode = SelectedComboText(GameOverlayModeBox, "TitleBar"),
OverlayCorner = SelectedComboText(GameOverlayCornerBox, "TopRight"),
EnvironmentToggles = BuildGameEnvironmentEntries(),
CustomEnvironmentVariables = PerGameSettings.Load(_gameSettingsTitleId)?.CustomEnvironmentVariables,
};
settings.RemoveInheritedValues(_settings);
settings.Save(_gameSettingsTitleId);
@ -385,9 +369,7 @@ public partial class MainWindow
var entries = new List<string>(_gameEnvironmentPassthrough);
foreach (var (name, toggle) in GameEnvironmentToggles())
{
if (name == StrictComputeSettings.VariableName)
StrictComputeSettings.SetEnabled(entries, toggle.IsChecked == true);
else if (toggle.IsChecked == true)
if (toggle.IsChecked == true)
{
entries.Add(name);
}
@ -405,9 +387,10 @@ public partial class MainWindow
?? choices.First(choice =>
string.Equals(choice.Value, fallback, StringComparison.OrdinalIgnoreCase));
private static bool IsEnvironmentEnabled(IEnumerable<string> entries, string name)
private static bool IsEnvironmentEnabled(
IEnumerable<string> entries,
string name)
{
if (name == StrictComputeSettings.VariableName) return StrictComputeSettings.IsEnabled(entries);
foreach (var entry in entries)
{
var parts = entry.Split('=', 2, StringSplitOptions.TrimEntries);
@ -461,7 +444,7 @@ public partial class MainWindow
private void SetGameOptionsPagesSpan(bool coversConsoleRow)
{
Grid.SetRowSpan(PagesHost, coversConsoleRow ? 3 : 1);
Grid.SetRowSpan(PagesHost, coversConsoleRow ? 2 : 1);
}
private Button[] GameOptionsNavigationButtons() =>
@ -491,9 +474,6 @@ public partial class MainWindow
private (string Name, ToggleSwitch Toggle)[] GameEnvironmentToggles() =>
[
("SHARPEMU_PROFILE_PERFORMANCE", GamePerformanceProfileToggle),
("SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE", GamePerformanceFrameTraceToggle),
(StrictComputeSettings.VariableName, GameStrictComputeToggle),
("SHARPEMU_BTHID_UNAVAILABLE", GameEnvBthidToggle),
("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", GameEnvLoopGuardToggle),
("SHARPEMU_WRITABLE_APP0", GameEnvWritableApp0Toggle),
@ -502,9 +482,9 @@ public partial class MainWindow
("SHARPEMU_LOG_DIRECT_MEMORY", GameEnvLogDirectMemoryToggle),
("SHARPEMU_LOG_IO", GameEnvLogIoToggle),
("SHARPEMU_LOG_NP", GameEnvLogNpToggle),
("SHARPEMU_GUEST_IMAGE_CPU_SYNC", GameEnvGuestImageCpuSyncToggle),
("SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES", GameEnvForceSubmitOrphanPreamblesToggle),
("SHARPEMU_RENDERDOC", GameEnvRenderDocToggle),
("SHARPEMU_VK_DISABLE_IMPLICITS", GameEnvDisableVkImplicitLayersToggle),
("SHARPEMU_CRASH_CAPTURE", GameCrashDumpToggle),
];
private static void SetGameOptionsOpenClass(Control control, bool active) =>

View File

@ -19,10 +19,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
KeyDown="OnKeyDown">
<Window.Resources>
<x:Double x:Key="LibraryCoverSize">148</x:Double>
<x:Double x:Key="LibraryTileWidth">160</x:Double>
<x:Double x:Key="LibraryRailItemHeight">172</x:Double>
<x:Double x:Key="LibraryGridItemHeight">200</x:Double>
<DataTemplate x:Key="LocalizedChoiceTemplate" x:DataType="local:LocalizedChoice">
<TextBlock Text="{Binding Label}"
HorizontalAlignment="Left"
@ -109,17 +105,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Grid>
<!-- Main content -->
<Grid x:Name="MainContent" Grid.Row="1" Margin="32,24,32,20">
<Grid.RowDefinitions>
<RowDefinition Height="Auto" />
<RowDefinition Height="*" MinHeight="160" />
<RowDefinition Height="0" />
<RowDefinition Height="0" />
</Grid.RowDefinitions>
<Grid x:Name="MainContent" Grid.Row="1" Margin="32,24,32,20" RowDefinitions="Auto,*,Auto">
<!-- Settings use the same opaque neutral surface as contextual
per-game settings, keeping both views in one visual system -->
<Border x:Name="OptionsPageSurface"
Grid.RowSpan="4"
Grid.RowSpan="3"
Margin="-32,-24,-32,-20"
Background="{StaticResource GameOptionsMenuBrush}"
IsVisible="False" />
@ -166,11 +156,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Panel Grid.Row="1" x:Name="PagesHost" ZIndex="1">
<!-- Library tiles use the available width and wrap before an item
reaches the window edge. -->
<Grid x:Name="LibraryPage" Margin="0,6,0,0" RowDefinitions="Auto,*">
<!-- Library page. Covers use a horizontal virtualized rail so the
number of realized images stays bounded for large libraries. -->
<Grid x:Name="LibraryPage" Margin="0,46,0,0" RowDefinitions="188,*">
<Panel x:Name="CarouselHost"
Grid.Row="1"
Grid.Row="0"
Classes="carouselHost"
RenderTransformOrigin="50%,50%">
<Panel.Transitions>
@ -239,8 +229,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
<DataTemplate x:DataType="local:GameEntry" x:CompileBindings="True">
<StackPanel Spacing="7">
<Border Classes="coverShadow libraryGameCard"
Width="{DynamicResource LibraryCoverSize}"
Height="{DynamicResource LibraryCoverSize}"
Width="148"
Height="148"
ToolTip.Tip="{Binding Name}"
AutomationProperties.Name="{Binding Name}">
<Border Classes="coverClip">
@ -267,8 +257,8 @@ SPDX-License-Identifier: GPL-2.0-or-later
</DataTemplate>
<DataTemplate x:DataType="local:AddFolderTile">
<Border Classes="addFolderTile"
Width="{DynamicResource LibraryCoverSize}"
Height="{DynamicResource LibraryCoverSize}"
Width="148"
Height="148"
ToolTip.Tip="{Binding [Library.AddFolder],
Source={x:Static local:Localization.Instance},
x:CompileBindings=False}"
@ -303,69 +293,52 @@ SPDX-License-Identifier: GPL-2.0-or-later
<!-- Selected-game identity and actions share one detail surface so
the selected title and metadata are never rendered twice. -->
<Grid x:Name="LibrarySelectedDetails"
Grid.Row="0"
Classes="selectedDetailsHost"
x:DataType="local:GameEntry"
x:CompileBindings="True"
IsVisible="False"
ColumnDefinitions="*,Auto"
RowDefinitions="Auto,Auto"
ColumnSpacing="20"
HorizontalAlignment="Stretch"
VerticalAlignment="Top"
Margin="4,0,4,10">
<Grid.Transitions>
<StackPanel x:Name="LibrarySelectedDetails"
Grid.Row="1"
Classes="selectedDetailsHost"
x:DataType="local:GameEntry"
x:CompileBindings="True"
IsVisible="False"
MaxWidth="980"
HorizontalAlignment="Left"
VerticalAlignment="Top"
Margin="4,8,0,0">
<StackPanel.Transitions>
<Transitions>
<DoubleTransition Property="Opacity"
Duration="0:0:0.16" />
<TransformOperationsTransition Property="RenderTransform"
Duration="0:0:0.18" />
</Transitions>
</Grid.Transitions>
<StackPanel Grid.Column="0" Spacing="7" VerticalAlignment="Center">
<TextBlock Classes="selectedGameTitle"
Text="{Binding Name}"
FontWeight="SemiBold"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="1" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Classes="pill" IsVisible="{Binding HasTitleId}">
<TextBlock Text="{Binding TitleId}" FontSize="11" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill" IsVisible="{Binding HasVersion}">
<TextBlock Text="{Binding VersionText}" FontSize="11" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill">
<TextBlock Text="{Binding SizeText}" FontSize="11" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
</StackPanel.Transitions>
<Border Classes="selectedDetailsDivider" />
<TextBlock Classes="selectedGameTitle"
Text="{Binding Name}"
FontWeight="SemiBold"
TextWrapping="Wrap"
TextTrimming="CharacterEllipsis"
MaxLines="2" />
<StackPanel Orientation="Horizontal" Spacing="8">
<Border Classes="pill" IsVisible="{Binding HasTitleId}">
<TextBlock Text="{Binding TitleId}" FontSize="11" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill" IsVisible="{Binding HasVersion}">
<TextBlock Text="{Binding VersionText}" FontSize="11" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
<Border Classes="pill">
<TextBlock Text="{Binding SizeText}" FontSize="11" FontWeight="SemiBold"
Foreground="{StaticResource MutedBrush}" />
</Border>
</StackPanel>
<StackPanel Grid.Column="1"
Orientation="Horizontal"
<StackPanel Orientation="Horizontal"
Spacing="12"
HorizontalAlignment="Right"
VerticalAlignment="Center">
HorizontalAlignment="Left">
<Button x:Name="LaunchButton"
Classes="playButton"
Content="Launch"
IsEnabled="False" />
<TextBlock Text="|"
Foreground="{StaticResource MutedBrush}"
FontSize="22"
VerticalAlignment="Center"
Margin="-4,0" />
<Button x:Name="LaunchCustomEnvButton"
Classes="optionsCircle"
ToolTip.Tip="{Binding [Launch.CustomEnv], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}"
AutomationProperties.Name="{Binding [Launch.CustomEnv], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}">
<TextBlock Classes="materialSymbol"
Text="keyboard_arrow_down" />
</Button>
<Button x:Name="GameSettingsButton"
Classes="optionsCircle"
ToolTip.Tip="{Binding [Library.Context.GameSettings],
@ -389,10 +362,7 @@ SPDX-License-Identifier: GPL-2.0-or-later
Text="terminal" />
</ToggleButton>
</StackPanel>
<Border Grid.Row="1"
Grid.ColumnSpan="2"
Classes="selectedDetailsDivider" />
</Grid>
</StackPanel>
<!-- Empty state -->
<StackPanel x:Name="EmptyState" Grid.RowSpan="2"
@ -806,18 +776,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ToggleSwitch x:Name="GameLogToFileToggle"
Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow x:Name="GameCrashDumpRow"
Classes="optionRow"
Label="{Binding [Options.CrashDump.Label],
Source={x:Static local:Localization.Instance},
x:CompileBindings=False}"
Description="{Binding [Options.CrashDump.Desc],
Source={x:Static local:Localization.Instance},
x:CompileBindings=False}">
<ToggleSwitch x:Name="GameCrashDumpToggle"
Classes="optionToggle" />
</local:SettingRow>
</StackPanel>
</Border>
</StackPanel>
@ -924,32 +882,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
HorizontalContentAlignment="Left"
VerticalAlignment="Center" />
</local:SettingRow>
<local:SettingRow x:Name="GameOverlayEnabledRow"
Classes="optionRow"
Label="{Binding [Options.OverlayEnabled.Label], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}"
Description="{Binding [Options.OverlayEnabled.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}">
<ToggleSwitch x:Name="GameOverlayEnabledToggle"
Classes="optionToggle"
ToolTip.Tip="{Binding [Options.OverlayEnabled.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}" />
</local:SettingRow>
<local:SettingRow x:Name="GameOverlayModeRow"
Classes="optionRow"
Label="{Binding [Options.OverlayMode.Label], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}"
Description="{Binding [Options.OverlayMode.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}">
<ComboBox x:Name="GameOverlayModeBox"
Classes="optionValue"
ToolTip.Tip="{Binding [Options.OverlayMode.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}"
ItemTemplate="{StaticResource LocalizedChoiceTemplate}" />
</local:SettingRow>
<local:SettingRow x:Name="GameOverlayCornerRow"
Classes="optionRow"
Label="{Binding [Options.OverlayCorner.Label], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}"
Description="{Binding [Options.OverlayCorner.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}">
<ComboBox x:Name="GameOverlayCornerBox"
Classes="optionValue"
ToolTip.Tip="{Binding [Options.OverlayCorner.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}"
ItemTemplate="{StaticResource LocalizedChoiceTemplate}" />
</local:SettingRow>
</StackPanel>
</Border>
</StackPanel>
@ -961,29 +893,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Border Classes="optionsGroup">
<StackPanel Spacing="8">
<TextBlock Classes="optionsGroupHeader" Text="{Binding [Options.Env.Group.Debug], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}" />
<local:SettingRow Classes="optionRow"
Label="SHARPEMU_PROFILE_PERFORMANCE"
Description="{Binding [Options.Env.PerformanceProfile.Desc],
Source={x:Static local:Localization.Instance},
x:CompileBindings=False}">
<ToggleSwitch x:Name="GamePerformanceProfileToggle"
Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow Classes="optionRow"
Label="SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE"
Description="{Binding [Options.Env.PerformanceFrameTrace.Desc],
Source={x:Static local:Localization.Instance},
x:CompileBindings=False}">
<ToggleSwitch x:Name="GamePerformanceFrameTraceToggle"
Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow Classes="optionRow"
Label="SHARPEMU_STRICT_COMPUTE"
Description="{Binding [Options.Env.StrictCompute.Desc],
Source={x:Static local:Localization.Instance},
x:CompileBindings=False}">
<ToggleSwitch x:Name="GameStrictComputeToggle" Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow Classes="optionRow"
Label="SHARPEMU_RENDERDOC"
Description="{Binding [Options.Env.RenderDoc.Desc],
@ -992,14 +901,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ToggleSwitch x:Name="GameEnvRenderDocToggle"
Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow Classes="optionRow"
Label="SHARPEMU_VK_DISABLE_IMPLICITS"
Description="{Binding [Options.Env.DisableVkImplicitLayers.Desc],
Source={x:Static local:Localization.Instance},
x:CompileBindings=False}">
<ToggleSwitch x:Name="GameEnvDisableVkImplicitLayersToggle"
Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow Classes="optionRow"
Label="SHARPEMU_VK_VALIDATION"
Description="{Binding [Options.Env.VkValidation.Desc],
@ -1068,6 +969,22 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ToggleSwitch x:Name="GameEnvWritableApp0Toggle"
Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow Classes="optionRow"
Label="SHARPEMU_GUEST_IMAGE_CPU_SYNC"
Description="{Binding [Options.Env.GuestImageCpuSync.Desc],
Source={x:Static local:Localization.Instance},
x:CompileBindings=False}">
<ToggleSwitch x:Name="GameEnvGuestImageCpuSyncToggle"
Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow Classes="optionRow"
Label="SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES"
Description="{Binding [Options.Env.ForceSubmitOrphanPreambles.Desc],
Source={x:Static local:Localization.Instance},
x:CompileBindings=False}">
<ToggleSwitch x:Name="GameEnvForceSubmitOrphanPreamblesToggle"
Classes="optionToggle" />
</local:SettingRow>
</StackPanel>
</Border>
</StackPanel>
@ -1185,28 +1102,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
VerticalAlignment="Center" />
</local:SettingRow>
<local:SettingRow x:Name="ConsoleTypeRow"
Classes="optionRow"
Label="{Binding [Options.Console.Type.Label], Source={x:Static local:Localization.Instance}}"
Description="{Binding [Options.Console.Type.Desc], Source={x:Static local:Localization.Instance}}">
<ComboBox x:Name="ConsoleTypeBox"
Classes="optionValue"
ItemTemplate="{StaticResource LocalizedChoiceTemplate}"
HorizontalContentAlignment="Left"
VerticalAlignment="Center" />
</local:SettingRow>
<local:SettingRow x:Name="ConsoleLanguageRow"
Classes="optionRow"
Label="{Binding [Options.Console.Language.Label], Source={x:Static local:Localization.Instance}}"
Description="{Binding [Options.Console.Language.Desc], Source={x:Static local:Localization.Instance}}">
<ComboBox x:Name="ConsoleLanguageBox"
Classes="optionValue"
ItemTemplate="{StaticResource LocalizedChoiceTemplate}"
HorizontalContentAlignment="Left"
VerticalAlignment="Center" />
</local:SettingRow>
<local:SettingRow x:Name="StrictRow"
Classes="optionRow"
Label="{Binding [Options.Strict.Label], Source={x:Static local:Localization.Instance}}"
@ -1254,14 +1149,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow x:Name="CrashDumpRow"
Classes="optionRow"
Label="{Binding [Options.CrashDump.Label], Source={x:Static local:Localization.Instance}}"
Description="{Binding [Options.CrashDump.Desc], Source={x:Static local:Localization.Instance}}">
<ToggleSwitch x:Name="CrashDumpToggle"
Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow x:Name="LogFilePathRow"
Classes="optionRow"
Label="{Binding [Options.LogFilePath.Label], Source={x:Static local:Localization.Instance}}"
@ -1455,11 +1342,12 @@ SPDX-License-Identifier: GPL-2.0-or-later
Foreground="{StaticResource SettingsDescriptionBrush}"
TextWrapping="Wrap" />
</StackPanel>
<Button Grid.Column="2"
x:Name="DiscordButton"
Classes="ghost"
Content="{Binding [About.DiscordButton], Source={x:Static local:Localization.Instance}}"
VerticalAlignment="Center" />
<TextBlock Grid.Column="2"
x:Name="DiscordComingSoonText"
Text="{Binding [About.DiscordComingSoon], Source={x:Static local:Localization.Instance}}"
FontSize="12"
Foreground="{StaticResource MutedBrush}"
VerticalAlignment="Center" />
</Grid>
</Border>
</StackPanel>
@ -1470,6 +1358,21 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ScrollViewer x:Name="OptionsRenderingPanel"
Classes="optionsSectionPanel">
<StackPanel Spacing="16">
<Border Classes="optionsGroup">
<StackPanel Spacing="8">
<local:SettingRow x:Name="RenderResolutionRow"
Classes="optionRow"
Label="{Binding [Options.RenderResolution.Label], Source={x:Static local:Localization.Instance}}"
Description="{Binding [Options.RenderResolution.Desc], Source={x:Static local:Localization.Instance}}">
<ComboBox x:Name="RenderResolutionBox"
Classes="optionValue"
ItemTemplate="{StaticResource LocalizedChoiceTemplate}"
HorizontalContentAlignment="Left"
VerticalAlignment="Center" />
</local:SettingRow>
</StackPanel>
</Border>
<Border Classes="optionsGroup">
<StackPanel Spacing="8">
<local:SettingRow x:Name="WindowModeRow"
@ -1535,32 +1438,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
HorizontalContentAlignment="Left"
VerticalAlignment="Center" />
</local:SettingRow>
<local:SettingRow x:Name="OverlayEnabledRow"
Classes="optionRow"
Label="{Binding [Options.OverlayEnabled.Label], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}"
Description="{Binding [Options.OverlayEnabled.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}">
<ToggleSwitch x:Name="OverlayEnabledToggle"
Classes="optionToggle"
ToolTip.Tip="{Binding [Options.OverlayEnabled.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}" />
</local:SettingRow>
<local:SettingRow x:Name="OverlayModeRow"
Classes="optionRow"
Label="{Binding [Options.OverlayMode.Label], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}"
Description="{Binding [Options.OverlayMode.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}">
<ComboBox x:Name="OverlayModeBox"
Classes="optionValue"
ToolTip.Tip="{Binding [Options.OverlayMode.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}"
ItemTemplate="{StaticResource LocalizedChoiceTemplate}" />
</local:SettingRow>
<local:SettingRow x:Name="OverlayCornerRow"
Classes="optionRow"
Label="{Binding [Options.OverlayCorner.Label], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}"
Description="{Binding [Options.OverlayCorner.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}">
<ComboBox x:Name="OverlayCornerBox"
Classes="optionValue"
ToolTip.Tip="{Binding [Options.OverlayCorner.Desc], Source={x:Static local:Localization.Instance}, x:CompileBindings=False}"
ItemTemplate="{StaticResource LocalizedChoiceTemplate}" />
</local:SettingRow>
</StackPanel>
</Border>
</StackPanel>
@ -1572,33 +1449,11 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Border Classes="optionsGroup">
<StackPanel Spacing="8">
<TextBlock Classes="optionsGroupHeader" Text="{Binding [Options.Env.Group.Debug], Source={x:Static local:Localization.Instance}}" />
<local:SettingRow x:Name="PerformanceProfileRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_PROFILE_PERFORMANCE"
Description="{Binding [Options.Env.PerformanceProfile.Desc], Source={x:Static local:Localization.Instance}}">
<ToggleSwitch x:Name="PerformanceProfileToggle" Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow x:Name="PerformanceFrameTraceRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE"
Description="{Binding [Options.Env.PerformanceFrameTrace.Desc], Source={x:Static local:Localization.Instance}}">
<ToggleSwitch x:Name="PerformanceFrameTraceToggle" Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_STRICT_COMPUTE"
Description="{Binding [Options.Env.StrictCompute.Desc], Source={x:Static local:Localization.Instance}}">
<ToggleSwitch x:Name="StrictComputeToggle" Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow x:Name="EnvRenderDocRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_RENDERDOC"
Description="{Binding [Options.Env.RenderDoc.Desc], Source={x:Static local:Localization.Instance}}">
<ToggleSwitch x:Name="EnvRenderDocToggle" Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow Classes="optionRow"
LabelFontFamily="Consolas,monospace"
Label="SHARPEMU_VK_DISABLE_IMPLICITS"
Description="{Binding [Options.Env.DisableVkImplicitLayers.Desc], Source={x:Static local:Localization.Instance}}">
<ToggleSwitch x:Name="EnvDisableVkImplicitLayersToggle" Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow x:Name="EnvVkValidationRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_VK_VALIDATION"
Description="{Binding [Options.Env.VkValidation.Desc], Source={x:Static local:Localization.Instance}}">
<ToggleSwitch x:Name="EnvVkValidationToggle" Classes="optionToggle" />
@ -1627,16 +1482,6 @@ SPDX-License-Identifier: GPL-2.0-or-later
<Border Classes="optionsGroupDivider" />
<TextBlock Classes="optionsGroupHeader" Text="{Binding [Options.Env.Group.General], Source={x:Static local:Localization.Instance}}" />
<local:SettingRow x:Name="BinkPlaybackRow"
Classes="optionRow"
Label="{Binding [Options.Env.BinkPlayback.Label], Source={x:Static local:Localization.Instance}}"
Description="{Binding [Options.Env.BinkPlayback.Desc], Source={x:Static local:Localization.Instance}}">
<ComboBox x:Name="BinkPlaybackBox"
Classes="optionValue"
ToolTip.Tip="{Binding [Options.Env.BinkPlayback.Desc], Source={x:Static local:Localization.Instance}}"
ItemTemplate="{StaticResource LocalizedChoiceTemplate}" />
</local:SettingRow>
<local:SettingRow x:Name="EnvBthidRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_BTHID_UNAVAILABLE"
Description="{Binding [Options.Env.Bthid.Desc], Source={x:Static local:Localization.Instance}}">
<ToggleSwitch x:Name="EnvBthidToggle" Classes="optionToggle" />
@ -1652,6 +1497,15 @@ SPDX-License-Identifier: GPL-2.0-or-later
<ToggleSwitch x:Name="EnvWritableApp0Toggle" Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow x:Name="EnvGuestImageCpuSyncRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_GUEST_IMAGE_CPU_SYNC"
Description="{Binding [Options.Env.GuestImageCpuSync.Desc], Source={x:Static local:Localization.Instance}}">
<ToggleSwitch x:Name="EnvGuestImageCpuSyncToggle" Classes="optionToggle" />
</local:SettingRow>
<local:SettingRow x:Name="EnvForceSubmitOrphanPreamblesRow" Classes="optionRow" LabelFontFamily="Consolas,monospace" Label="SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES"
Description="{Binding [Options.Env.ForceSubmitOrphanPreambles.Desc], Source={x:Static local:Localization.Instance}}">
<ToggleSwitch x:Name="EnvForceSubmitOrphanPreamblesToggle" Classes="optionToggle" />
</local:SettingRow>
</StackPanel>
</Border>
@ -1662,18 +1516,9 @@ SPDX-License-Identifier: GPL-2.0-or-later
</Grid>
</Panel>
<GridSplitter x:Name="ConsoleSplitter"
Grid.Row="2"
Height="8"
ResizeDirection="Rows"
ResizeBehavior="PreviousAndNext"
Background="Transparent"
Cursor="SizeNorthSouth"
IsVisible="False" />
<!-- Console (collapsible) -->
<Border Grid.Row="3" x:Name="ConsolePanel" Classes="card" Padding="0"
IsVisible="False">
<Border Grid.Row="2" x:Name="ConsolePanel" Classes="card" Padding="0" Height="240"
Margin="0,12,0,0" IsVisible="False">
<Grid RowDefinitions="Auto,*">
<Grid Grid.Row="0" ColumnDefinitions="*,Auto,Auto,Auto,Auto,Auto,Auto" Margin="16,12,16,8">
<TextBlock x:Name="ConsoleSectionTitle" Classes="sectionTitle"

View File

@ -48,44 +48,6 @@ public partial class MainWindow : Window
[
LocalizedChoice.FromKey("Native", "Options.CpuEngine.Native"),
];
private readonly LocalizedChoice[] _consoleTypeChoices =
[
LocalizedChoice.Literal("PS5", "PlayStation 5"),
];
private readonly LocalizedChoice[] _consoleLanguageChoices =
[
LocalizedChoice.Literal("Japanese", "日本語"),
LocalizedChoice.Literal("EnglishUS", "English (United States)"),
LocalizedChoice.Literal("French", "Français"),
LocalizedChoice.Literal("SpanishSpain", "Español (España)"),
LocalizedChoice.Literal("German", "Deutsch"),
LocalizedChoice.Literal("Italian", "Italiano"),
LocalizedChoice.Literal("Dutch", "Nederlands"),
LocalizedChoice.Literal("PortuguesePortugal", "Português (Portugal)"),
LocalizedChoice.Literal("Russian", "Русский"),
LocalizedChoice.Literal("Korean", "한국어"),
LocalizedChoice.Literal("ChineseTraditional", "繁體中文"),
LocalizedChoice.Literal("ChineseSimplified", "简体中文"),
LocalizedChoice.Literal("Finnish", "Suomi"),
LocalizedChoice.Literal("Swedish", "Svenska"),
LocalizedChoice.Literal("Danish", "Dansk"),
LocalizedChoice.Literal("Norwegian", "Norsk"),
LocalizedChoice.Literal("Polish", "Polski"),
LocalizedChoice.Literal("PortugueseBrazil", "Português (Brasil)"),
LocalizedChoice.Literal("EnglishUK", "English (United Kingdom)"),
LocalizedChoice.Literal("Turkish", "Türkçe"),
LocalizedChoice.Literal("SpanishLatinAmerica", "Español (Latinoamérica)"),
LocalizedChoice.Literal("Arabic", "العربية"),
LocalizedChoice.Literal("FrenchCanada", "Français (Canada)"),
LocalizedChoice.Literal("Czech", "Čeština"),
LocalizedChoice.Literal("Hungarian", "Magyar"),
LocalizedChoice.Literal("Greek", "Ελληνικά"),
LocalizedChoice.Literal("Romanian", "Română"),
LocalizedChoice.Literal("Thai", "ไทย"),
LocalizedChoice.Literal("Vietnamese", "Tiếng Việt"),
LocalizedChoice.Literal("Indonesian", "Bahasa Indonesia"),
LocalizedChoice.Literal("Ukrainian", "Українська"),
];
private readonly LocalizedChoice[] _logLevelChoices =
[
LocalizedChoice.FromKey("Trace", "Options.LogLevel.Trace"),
@ -95,6 +57,13 @@ public partial class MainWindow : Window
LocalizedChoice.FromKey("Error", "Options.LogLevel.Error"),
LocalizedChoice.FromKey("Critical", "Options.LogLevel.Critical"),
];
private readonly LocalizedChoice[] _renderResolutionChoices =
[
LocalizedChoice.FromKey("1.0", "Options.RenderResolution.Native"),
LocalizedChoice.Literal("0.75", "75%"),
LocalizedChoice.Literal("0.5", "50%"),
LocalizedChoice.Literal("0.25", "25%"),
];
private readonly LocalizedChoice[] _windowModeChoices =
[
LocalizedChoice.FromKey("Windowed", "Options.WindowMode.Windowed"),
@ -108,31 +77,12 @@ public partial class MainWindow : Window
LocalizedChoice.FromKey("Stretch", "Options.Scaling.Stretch"),
LocalizedChoice.FromKey("Integer", "Options.Scaling.Integer"),
];
private readonly LocalizedChoice[] _overlayModeChoices =
[
LocalizedChoice.FromKey("Full", "Options.OverlayMode.Full"),
LocalizedChoice.FromKey("Minimal", "Options.OverlayMode.Minimal"),
LocalizedChoice.FromKey("TitleBar", "Options.OverlayMode.TitleBar"),
];
private readonly LocalizedChoice[] _overlayCornerChoices =
[
LocalizedChoice.FromKey("TopLeft", "Options.OverlayCorner.TopLeft"),
LocalizedChoice.FromKey("TopRight", "Options.OverlayCorner.TopRight"),
LocalizedChoice.FromKey("BottomRight", "Options.OverlayCorner.BottomRight"),
LocalizedChoice.FromKey("BottomLeft", "Options.OverlayCorner.BottomLeft"),
];
private readonly LocalizedChoice[] _hdrModeChoices =
[
LocalizedChoice.FromKey("Auto", "Options.Hdr.Auto"),
LocalizedChoice.FromKey("On", "Common.On"),
LocalizedChoice.FromKey("Off", "Common.Off"),
];
private readonly LocalizedChoice[] _binkPlaybackChoices =
[
LocalizedChoice.FromKey("Host", "Options.Env.BinkPlayback.Host"),
LocalizedChoice.FromKey("Guest", "Options.Env.BinkPlayback.Guest"),
LocalizedChoice.FromKey("Skip", "Options.Env.BinkPlayback.Skip"),
];
private readonly List<GameEntry> _allGames = new();
private readonly ObservableCollection<GameEntry> _visibleGames = new();
private readonly LibraryTileCollection _libraryTiles;
@ -141,7 +91,6 @@ public partial class MainWindow : Window
private readonly List<LogLine> _allConsoleLines = new();
private readonly ConcurrentQueue<(string Line, bool IsError)> _pendingLines = new();
private readonly DispatcherTimer _consoleFlushTimer;
private readonly DispatcherTimer _libraryLayoutTimer;
private GuiSettings _settings = new();
private IReadOnlyList<HostDisplayOption> _hostDisplays = [];
@ -176,8 +125,6 @@ public partial class MainWindow : Window
private bool _addFolderInProgress;
private bool _isLibraryGridLayout;
private GameEntry? _lastSelectedGame;
private double _libraryRailRowHeight = 188;
private double _libraryGridRowHeight = 216;
// Bundled key art shown whenever no game-specific backdrop applies; the
// plain window color remains the fallback when the asset fails to load.
@ -199,8 +146,6 @@ public partial class MainWindow : Window
string EbootPath,
string DisplayName,
string? TitleId,
string ConsoleType,
string ConsoleLanguage,
EffectiveLaunchSettings Settings,
SharpEmuRuntimeOptions RuntimeOptions);
@ -237,15 +182,6 @@ public partial class MainWindow : Window
MaybeAutoScroll();
};
_consoleFlushTimer.Start();
_libraryLayoutTimer = new DispatcherTimer
{
Interval = TimeSpan.FromMilliseconds(120),
};
_libraryLayoutTimer.Tick += (_, _) =>
{
_libraryLayoutTimer.Stop();
UpdateLibraryLayoutMetrics();
};
TitleBar.PointerPressed += OnTitleBarPointerPressed;
TitleBar.DoubleTapped += OnTitleBarDoubleTapped;
@ -280,7 +216,6 @@ public partial class MainWindow : Window
LaunchSelected();
}
};
LaunchCustomEnvButton.Click += async (_, _) => await OpenCustomEnvironmentDialogAsync();
ClearLogButton.Click += (_, _) => { _consoleLines.Clear(); _allConsoleLines.Clear(); };
CopyLogButton.Click += async (_, _) => await CopyConsoleAsync();
DetachConsoleButton.Click += (_, _) => ShowConsoleWindow();
@ -288,10 +223,9 @@ public partial class MainWindow : Window
LibraryTabButton.Click += (_, _) => SetActivePage(0);
OptionsTabButton.Click += (_, _) => SetActivePage(1);
LibraryLayoutButton.Click += (_, _) => ToggleLibraryLayout();
LibraryPage.SizeChanged += (_, _) => ScheduleLibraryLayoutMetricsUpdate();
LibrarySelectedDetails.SizeChanged += (_, _) => UpdateLibraryMinimumHeight();
MainContent.SizeChanged += (_, _) => ClampEmbeddedConsoleHeight();
ConsoleToggle.IsCheckedChanged += (_, _) => UpdateEmbeddedConsoleVisibility();
LibraryPage.SizeChanged += (_, _) => UpdateLibraryGridHeight();
LibrarySelectedDetails.SizeChanged += (_, _) => UpdateLibraryGridHeight();
ConsoleToggle.IsCheckedChanged += (_, _) => ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
WireOptionsNavigation();
WireGameOptions();
@ -299,6 +233,18 @@ public partial class MainWindow : Window
// it is open already uses the new values.
LogLevelBox.SelectionChanged += (_, _) => _settings.LogLevel = SelectedLogLevel();
TraceImportsBox.ValueChanged += (_, _) => _settings.ImportTraceLimit = (int)(TraceImportsBox.Value ?? 0);
RenderResolutionBox.SelectionChanged += (_, _) =>
{
if (RenderResolutionBox.SelectedItem is LocalizedChoice { Value: var value } &&
double.TryParse(
value,
System.Globalization.NumberStyles.Float,
System.Globalization.CultureInfo.InvariantCulture,
out var scale))
{
_settings.RenderResolutionScale = scale;
}
};
StrictToggle.IsCheckedChanged += (_, _) => _settings.StrictDynlibResolution = StrictToggle.IsChecked == true;
LogToFileToggle.IsCheckedChanged += (_, _) => _settings.LogToFile = LogToFileToggle.IsChecked == true;
OverrideLogFileToggle.IsCheckedChanged += (_, _) =>
@ -315,10 +261,6 @@ public partial class MainWindow : Window
};
AutoUpdateToggle.IsCheckedChanged += (_, _) =>
_settings.CheckForUpdatesOnStartup = AutoUpdateToggle.IsChecked == true;
ConsoleTypeBox.SelectionChanged += (_, _) =>
_settings.ConsoleType = SelectedComboText(ConsoleTypeBox, "PS5");
ConsoleLanguageBox.SelectionChanged += (_, _) =>
_settings.ConsoleLanguage = SelectedComboText(ConsoleLanguageBox, "EnglishUS");
WindowModeBox.SelectionChanged += (_, _) => _settings.WindowMode = SelectedComboText(WindowModeBox, "Windowed");
DisplayBox.SelectionChanged += (_, _) => OnHostDisplayChanged();
ResolutionBox.SelectionChanged += (_, _) => OnHostResolutionChanged();
@ -326,25 +268,14 @@ public partial class MainWindow : Window
ScalingModeBox.SelectionChanged += (_, _) => _settings.ScalingMode = SelectedComboText(ScalingModeBox, "Fit");
VSyncToggle.IsCheckedChanged += (_, _) => _settings.VSync = VSyncToggle.IsChecked == true;
HdrModeBox.SelectionChanged += (_, _) => _settings.HdrMode = SelectedComboText(HdrModeBox, "Auto");
OverlayEnabledToggle.IsCheckedChanged += (_, _) => _settings.OverlayEnabled = OverlayEnabledToggle.IsChecked == true;
OverlayModeBox.SelectionChanged += (_, _) => _settings.OverlayMode = SelectedComboText(OverlayModeBox, "TitleBar");
OverlayCornerBox.SelectionChanged += (_, _) => _settings.OverlayCorner = SelectedComboText(OverlayCornerBox, "TopRight");
UpdateButton.Click += async (_, _) => await OnUpdateButtonAsync();
SelectLogFilePathButton.Click += async (_, _) => await SelectLogFilePathAsync();
PerformanceProfileToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_PROFILE_PERFORMANCE", PerformanceProfileToggle.IsChecked == true);
PerformanceFrameTraceToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE", PerformanceFrameTraceToggle.IsChecked == true);
StrictComputeToggle.IsCheckedChanged += (_, _) =>
StrictComputeSettings.SetEnabled(_settings.EnvironmentToggles, StrictComputeToggle.IsChecked == true);
EnvBthidToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_BTHID_UNAVAILABLE", EnvBthidToggle.IsChecked == true);
EnvLoopGuardToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD", EnvLoopGuardToggle.IsChecked == true);
EnvWritableApp0Toggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_WRITABLE_APP0", EnvWritableApp0Toggle.IsChecked == true);
BinkPlaybackBox.SelectionChanged += (_, _) =>
_settings.BinkPlaybackMode = SelectedComboText(BinkPlaybackBox, "Guest");
EnvVkValidationToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_VK_VALIDATION", EnvVkValidationToggle.IsChecked == true);
EnvDumpSpirvToggle.IsCheckedChanged += (_, _) =>
@ -355,8 +286,14 @@ public partial class MainWindow : Window
SetEnvironmentToggle("SHARPEMU_LOG_IO", EnvLogIoToggle.IsChecked == true);
EnvLogNpToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_LOG_NP", EnvLogNpToggle.IsChecked == true);
CrashDumpToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle("SHARPEMU_CRASH_CAPTURE", CrashDumpToggle.IsChecked == true);
EnvGuestImageCpuSyncToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle(
"SHARPEMU_GUEST_IMAGE_CPU_SYNC",
EnvGuestImageCpuSyncToggle.IsChecked == true);
EnvForceSubmitOrphanPreamblesToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle(
"SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES",
EnvForceSubmitOrphanPreamblesToggle.IsChecked == true);
DefaultProfileBox.TextChanged += (_, _) =>
_settings.DefaultProfile = GuiSettings.NormalizeDefaultProfile(DefaultProfileBox.Text);
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
@ -386,10 +323,6 @@ public partial class MainWindow : Window
SetEnvironmentToggle(
"SHARPEMU_RENDERDOC",
EnvRenderDocToggle.IsChecked == true);
EnvDisableVkImplicitLayersToggle.IsCheckedChanged += (_, _) =>
SetEnvironmentToggle(
"SHARPEMU_VK_DISABLE_IMPLICITS",
EnvDisableVkImplicitLayersToggle.IsChecked == true);
DefaultProfileBox.TextChanged += (_, _) =>
_settings.DefaultProfile = GuiSettings.NormalizeDefaultProfile(DefaultProfileBox.Text);
LanguageBox.SelectionChanged += (_, _) => OnLanguageChanged();
@ -422,16 +355,7 @@ public partial class MainWindow : Window
{
Process.Start(new ProcessStartInfo
{
FileName = ProjectLinks.RepositoryUrl,
UseShellExecute = true
});
};
DiscordButton.Click += (_, _) =>
{
Process.Start(new ProcessStartInfo
{
FileName = "https://discord.com/invite/zdTuUU9Uwn",
FileName = "https://github.com/sharpemu/sharpemu",
UseShellExecute = true
});
};
@ -445,7 +369,8 @@ public partial class MainWindow : Window
Process.Start(new ProcessStartInfo
{
FileName = ProjectLinks.CommitUrl(_latestCommitSha),
FileName =
$"https://github.com/sharpemu/sharpemu/commit/{_latestCommitSha}",
UseShellExecute = true
});
};
@ -484,7 +409,6 @@ public partial class MainWindow : Window
LibraryToolbar.IsVisible = index == 0;
OptionsPageSurface.IsVisible = index == 1;
OptionsPage.IsVisible = index == 1;
UpdateEmbeddedConsoleVisibility();
if (index == 1)
{
@ -499,8 +423,13 @@ public partial class MainWindow : Window
_isLibraryGridLayout = grid;
SetClass(GameList, "gridLayout", grid);
SetClass(LibrarySelectedDetails, "gridLayout", grid);
LibraryPage.Margin = new Thickness(0, 6, 0, 0);
UpdateLibraryLayoutMetrics();
LibraryPage.RowDefinitions[0].Height = grid
? GridLength.Auto
: new GridLength(188);
LibraryPage.Margin = grid
? new Thickness(0, 6, 0, 0)
: new Thickness(0, 46, 0, 0);
UpdateLibraryGridHeight();
UpdateLibraryLayoutButton();
if (GameList.SelectedItem is { } selected)
@ -511,37 +440,18 @@ public partial class MainWindow : Window
}
}
private void UpdateLibraryLayoutMetrics()
private void UpdateLibraryGridHeight()
{
var metrics = LibraryLayoutMetrics.Calculate(LibraryPage.Bounds.Width);
Resources["LibraryCoverSize"] = metrics.CoverSize;
Resources["LibraryTileWidth"] = metrics.ItemWidth;
Resources["LibraryRailItemHeight"] = metrics.RailItemHeight;
Resources["LibraryGridItemHeight"] = metrics.GridItemHeight;
_libraryRailRowHeight = metrics.RailItemHeight + 16;
_libraryGridRowHeight = metrics.GridItemHeight + 16;
var pageHeight = LibraryPage.Bounds.Height;
if (!_isLibraryGridLayout || pageHeight <= 0)
{
GameList.MaxHeight = double.PositiveInfinity;
return;
}
GameList.MaxHeight = double.PositiveInfinity;
UpdateLibraryMinimumHeight();
}
private void ScheduleLibraryLayoutMetricsUpdate()
{
_libraryLayoutTimer.Stop();
_libraryLayoutTimer.Start();
}
private void UpdateLibraryMinimumHeight()
{
var detailsHeight = LibrarySelectedDetails.IsVisible
? LibrarySelectedDetails.DesiredSize.Height
: 0;
var cardRowHeight = _isLibraryGridLayout
? _libraryGridRowHeight
: _libraryRailRowHeight;
MainContent.RowDefinitions[1].MinHeight =
LibraryPage.Margin.Top + detailsHeight + cardRowHeight;
ClampEmbeddedConsoleHeight();
GameList.MaxHeight = Math.Max(
0,
pageHeight - LibrarySelectedDetails.DesiredSize.Height);
}
private void ToggleLibraryLayout()
@ -739,13 +649,16 @@ public partial class MainWindow : Window
}
private async Task LoadLatestCommitAsync()
{
const string apiUrl =
"https://api.github.com/repos/sharpemu/sharpemu/commits/main";
_latestCommitSha = null;
LatestCommitHashText.Content = "Loading…";
LatestCommitHashText.IsEnabled = false;
try
{
using var response = await GithubHttpClient.GetAsync(ProjectLinks.LatestCommitApiUrl);
using var response = await GithubHttpClient.GetAsync(apiUrl);
var responseBody =
(await response.Content.ReadAsStringAsync()).Trim();
@ -1132,14 +1045,12 @@ public partial class MainWindow : Window
Interlocked.Increment(ref _libraryScanGeneration);
Interlocked.Increment(ref _detailLoadGeneration);
_consoleFlushTimer.Stop();
_libraryLayoutTimer.Stop();
_gamepadTimer.Stop();
}
private void CompleteWindowClosing()
{
RunShutdownStep("library watcher", _libraryWatcher.Dispose);
RememberEmbeddedConsoleHeight();
RunShutdownStep("settings", _settings.Save);
RunShutdownStep("SDL gamepad", SdlLauncherGamepad.Shutdown);
RunShutdownStep("title music", _sndPreview.Stop);
@ -1147,7 +1058,6 @@ public partial class MainWindow : Window
RunShutdownStep("console window", () => _consoleWindow?.Close());
RunShutdownStep("emulator process", () => _emulator?.Dispose());
RunShutdownStep("console mirror", () => _consoleMirror?.Dispose());
RunShutdownStep("pending console output", FlushAllPendingConsoleLines);
RunShutdownStep("file log", DropFileLog);
}
@ -1244,29 +1154,21 @@ public partial class MainWindow : Window
private void InitializeLocalizedChoiceBoxes()
{
CpuEngineBox.ItemsSource = _cpuEngineChoices;
ConsoleTypeBox.ItemsSource = _consoleTypeChoices;
ConsoleLanguageBox.ItemsSource = _consoleLanguageChoices;
LogLevelBox.ItemsSource = _logLevelChoices;
RenderResolutionBox.ItemsSource = _renderResolutionChoices;
WindowModeBox.ItemsSource = _windowModeChoices;
ScalingModeBox.ItemsSource = _scalingModeChoices;
HdrModeBox.ItemsSource = _hdrModeChoices;
BinkPlaybackBox.ItemsSource = _binkPlaybackChoices;
OverlayModeBox.ItemsSource = _overlayModeChoices;
OverlayCornerBox.ItemsSource = _overlayCornerChoices;
}
private void RefreshLocalizedChoices()
{
RefreshChoices(_cpuEngineChoices);
RefreshChoices(_consoleTypeChoices);
RefreshChoices(_consoleLanguageChoices);
RefreshChoices(_logLevelChoices);
RefreshChoices(_renderResolutionChoices);
RefreshChoices(_windowModeChoices);
RefreshChoices(_scalingModeChoices);
RefreshChoices(_hdrModeChoices);
RefreshChoices(_binkPlaybackChoices);
RefreshChoices(_overlayModeChoices);
RefreshChoices(_overlayCornerChoices);
}
private static void RefreshChoices(IEnumerable<LocalizedChoice> choices)
@ -1280,10 +1182,6 @@ public partial class MainWindow : Window
private void ApplySettingsToControls()
{
CpuEngineBox.SelectedIndex = 0;
ConsoleTypeBox.SelectedIndex = 0;
ConsoleLanguageBox.SelectedIndex = ChoiceIndex(
_settings.ConsoleLanguage,
_consoleLanguageChoices.Select(choice => choice.Value).ToArray());
LogLevelBox.SelectedIndex = _settings.LogLevel.ToLowerInvariant() switch
{
"trace" => 0,
@ -1295,39 +1193,40 @@ public partial class MainWindow : Window
_ => 2,
};
TraceImportsBox.Value = Math.Clamp(_settings.ImportTraceLimit, 0, 4096);
RenderResolutionBox.SelectedIndex = _settings.RenderResolutionScale switch
{
>= 0.875 => 0,
>= 0.625 => 1,
>= 0.375 => 2,
_ => 3,
};
StrictToggle.IsChecked = _settings.StrictDynlibResolution;
LogToFileToggle.IsChecked = _settings.LogToFile;
CrashDumpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_CRASH_CAPTURE");
OverrideLogFileToggle.IsChecked = _settings.OverrideLogFile;
TitleMusicToggle.IsChecked = _settings.PlayTitleMusic;
SetLibraryLayout(string.Equals(_settings.LibraryLayout, "Grid", StringComparison.OrdinalIgnoreCase));
DiscordToggle.IsChecked = _settings.DiscordRichPresence;
AutoUpdateToggle.IsChecked = _settings.CheckForUpdatesOnStartup;
PerformanceProfileToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_PROFILE_PERFORMANCE");
PerformanceFrameTraceToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE");
StrictComputeToggle.IsChecked = StrictComputeSettings.IsEnabled(_settings.EnvironmentToggles);
EnvBthidToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_BTHID_UNAVAILABLE");
EnvLoopGuardToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
EnvWritableApp0Toggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_WRITABLE_APP0");
BinkPlaybackBox.SelectedIndex = ChoiceIndex(_settings.BinkPlaybackMode, "Host", "Guest", "Skip");
EnvVkValidationToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_VK_VALIDATION");
EnvDumpSpirvToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_DUMP_SPIRV");
EnvLogDirectMemoryToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_DIRECT_MEMORY");
EnvLogIoToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_IO");
EnvLogNpToggle.IsChecked = _settings.EnvironmentToggles.Contains("SHARPEMU_LOG_NP");
EnvGuestImageCpuSyncToggle.IsChecked =
_settings.EnvironmentToggles.Contains("SHARPEMU_GUEST_IMAGE_CPU_SYNC");
EnvForceSubmitOrphanPreamblesToggle.IsChecked =
_settings.EnvironmentToggles.Contains("SHARPEMU_FORCE_SUBMIT_ORPHAN_PREAMBLES");
EnvRenderDocToggle.IsChecked =
_settings.EnvironmentToggles.Contains("SHARPEMU_RENDERDOC");
EnvDisableVkImplicitLayersToggle.IsChecked =
_settings.EnvironmentToggles.Contains("SHARPEMU_VK_DISABLE_IMPLICITS");
DefaultProfileBox.Text = _settings.DefaultProfile;
WindowModeBox.SelectedIndex = ChoiceIndex(_settings.WindowMode, "Windowed", "Borderless", "Exclusive");
LoadHostDisplayOptions();
ScalingModeBox.SelectedIndex = ChoiceIndex(_settings.ScalingMode, "Fit", "Cover", "Stretch", "Integer");
VSyncToggle.IsChecked = _settings.VSync;
HdrModeBox.SelectedIndex = ChoiceIndex(_settings.HdrMode, "Auto", "On", "Off");
OverlayEnabledToggle.IsChecked = _settings.OverlayEnabled;
OverlayModeBox.SelectedIndex = ChoiceIndex(_settings.OverlayMode, "Full", "Minimal", "TitleBar");
OverlayCornerBox.SelectedIndex = ChoiceIndex(_settings.OverlayCorner, "TopLeft", "TopRight", "BottomRight", "BottomLeft");
UpdateLogFilePathText();
}
@ -1550,7 +1449,6 @@ public partial class MainWindow : Window
}
private const string DefaultProfileEnvironmentName = "SHARPEMU_DEFAULT_PROFILE";
private const string BinkModeEnvironmentName = "SHARPEMU_BINK_MODE";
private string SelectedLogLevel()
{
@ -2224,7 +2122,10 @@ public partial class MainWindow : Window
GameLibraryReconciler.ReconcileVisibleGames(_visibleGames, desired);
var selectedAfter = ResolveLibrarySelection(_visibleGames, selectedPath);
var selectedAfter = selectedPath is null
? null
: _visibleGames.FirstOrDefault(game =>
game.Path.Equals(selectedPath, GameLibraryPath.Comparison));
if (!ReferenceEquals(GameList.SelectedItem, selectedAfter))
{
GameList.SelectedItem = selectedAfter;
@ -2247,17 +2148,6 @@ public partial class MainWindow : Window
}
}
internal static GameEntry? ResolveLibrarySelection(
IReadOnlyList<GameEntry> visibleGames,
string? selectedPath)
{
var selected = selectedPath is null
? null
: visibleGames.FirstOrDefault(game =>
game.Path.Equals(selectedPath, GameLibraryPath.Comparison));
return selected ?? visibleGames.FirstOrDefault();
}
/// <summary>
/// Refreshes the empty-state title/hint from the current language and
/// search text; a no-op while the empty state is not showing.
@ -2296,7 +2186,6 @@ public partial class MainWindow : Window
_sndPreview.Stop();
}
UpdateLibraryMinimumHeight();
UpdateRunButtons();
}
@ -2432,34 +2321,6 @@ public partial class MainWindow : Window
}
}
private async Task OpenCustomEnvironmentDialogAsync()
{
if (_isRunning || GameList.SelectedItem is not GameEntry game)
{
return;
}
var perGame = PerGameSettings.Load(game.TitleId);
var initialEntries = CustomEnvironmentVariables.Merge(
GlobalCustomEnvironmentSettings.Load(),
perGame?.CustomEnvironmentVariables);
var dialog = new CustomEnvironmentDialog(game.TitleId, initialEntries);
dialog.SaveGlobalRequested += entries => GlobalCustomEnvironmentSettings.Save(entries);
dialog.SaveGameRequested += (titleId, entries) =>
{
var settings = PerGameSettings.Load(titleId) ?? new PerGameSettings();
settings.CustomEnvironmentVariables = entries.Count == 0 ? null : entries.ToList();
settings.RemoveInheritedValues(_settings);
settings.Save(titleId);
};
var customEntries = await dialog.ShowDialog<IReadOnlyList<string>?>(this);
if (customEntries is not null && !_isRunning)
{
Launch(game.Path, game.Name, game.TitleId, customEntries);
}
}
private void LaunchSelected()
{
if (GameList.SelectedItem is GameEntry game)
@ -2468,11 +2329,7 @@ public partial class MainWindow : Window
}
}
private void Launch(
string ebootPath,
string displayName,
string? titleId = null,
IReadOnlyList<string>? oneShotCustomEnvironment = null)
private void Launch(string ebootPath, string displayName, string? titleId = null)
{
if (_isRunning)
{
@ -2483,12 +2340,7 @@ public partial class MainWindow : Window
? _allGames.FirstOrDefault(game =>
game.Path.Equals(ebootPath, GameLibraryPath.Comparison))?.TitleId
: titleId;
var perGame = PerGameSettings.Load(resolvedTitleId);
var effective = EffectiveLaunchSettings.Resolve(_settings, perGame);
var customEnvironment = CustomEnvironmentVariables.Merge(
GlobalCustomEnvironmentSettings.Load(),
perGame?.CustomEnvironmentVariables,
oneShotCustomEnvironment);
var effective = EffectiveLaunchSettings.Resolve(_settings, PerGameSettings.Load(resolvedTitleId));
_sndPreview.Stop();
_consoleLines.Clear();
@ -2529,54 +2381,16 @@ public partial class MainWindow : Window
_appliedEnvironmentVariables.Add(name);
}
if (RenderDocCapture.ApplyVulkanLoaderEnvironment())
{
_appliedEnvironmentVariables.Add("VK_LOADER_DEBUG");
_appliedEnvironmentVariables.Add("VK_LOADER_LAYERS_DISABLE");
_appliedEnvironmentVariables.Add("VK_LOADER_LAYERS_ALLOW");
}
Environment.SetEnvironmentVariable(
DefaultProfileEnvironmentName,
GuiSettings.NormalizeDefaultProfile(_settings.DefaultProfile));
// Apply both values so an inherited skip setting cannot override the GUI choice.
Environment.SetEnvironmentVariable(StrictComputeSettings.VariableName,
StrictComputeSettings.GetLaunchValue(effective.EnvironmentToggles));
_appliedEnvironmentVariables.Add(StrictComputeSettings.VariableName);
_appliedEnvironmentVariables.Add(DefaultProfileEnvironmentName);
// An absent switch now enables the guard opt-out by default; send 0 for a disabled GUI toggle.
Environment.SetEnvironmentVariable("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD",
effective.EnvironmentToggles.Contains("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD") ? "1" : "0");
_appliedEnvironmentVariables.Add("SHARPEMU_DISABLE_IMPORT_LOOP_GUARD");
Environment.SetEnvironmentVariable(
BinkModeEnvironmentName,
_settings.BinkPlaybackMode switch
{
"Guest" => "guest",
"Skip" => "skip",
_ => "native",
});
_appliedEnvironmentVariables.Add(BinkModeEnvironmentName);
Environment.SetEnvironmentVariable(
"SHARPEMU_RENDER_SCALE",
_settings.RenderResolutionScale.ToString(
"0.###",
System.Globalization.CultureInfo.InvariantCulture));
_appliedEnvironmentVariables.Add("SHARPEMU_RENDER_SCALE");
foreach (var entry in customEnvironment)
{
if (!CustomEnvironmentVariables.TryParseEntry(entry, out var name, out var value))
{
continue;
}
Environment.SetEnvironmentVariable(name, value);
_appliedEnvironmentVariables.Add(name);
}
if (SharpEmuLog.TryParseLevel(effective.LogLevel, out var logLevel))
{
@ -2602,8 +2416,6 @@ public partial class MainWindow : Window
Path.GetFullPath(ebootPath),
displayName,
_runningGameTitleId,
_settings.ConsoleType,
_settings.ConsoleLanguage,
effective,
runtimeOptions);
@ -2650,7 +2462,7 @@ public partial class MainWindow : Window
_runningGameName = null;
_runningGameTitleId = null;
UpdateDiscordPresence();
UpdateEmbeddedConsoleVisibility();
ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
Console.Error.WriteLine("[GUI][INFO] Waiting for the SDL game process to exit.");
}
@ -2688,13 +2500,12 @@ public partial class MainWindow : Window
private void OnEmulatorExited(int exitCode)
{
FlushPendingConsoleLines();
FlushAllPendingConsoleLines();
_isRunning = false;
_isStopping = false;
_emulator?.Dispose();
_emulator = null;
_pendingLaunch = null;
UpdateEmbeddedConsoleVisibility();
ConsolePanel.IsVisible = ConsoleToggle.IsChecked == true && _consoleWindow is null;
var meaningKey = exitCode switch
{
@ -2770,8 +2581,6 @@ public partial class MainWindow : Window
{
"--cpu-engine=native",
$"--log-level={launch.Settings.LogLevel}",
$"--console={launch.ConsoleType.ToLowerInvariant()}",
$"--console-language={launch.ConsoleLanguage}",
};
if (launch.RuntimeOptions.StrictDynlibResolution)
{
@ -2789,9 +2598,6 @@ public partial class MainWindow : Window
arguments.Add($"--scaling={launch.Settings.ScalingMode.ToLowerInvariant()}");
arguments.Add($"--vsync={(launch.Settings.VSync ? "on" : "off")}");
arguments.Add($"--hdr={launch.Settings.HdrMode.ToLowerInvariant()}");
arguments.Add($"--overlay={(launch.Settings.OverlayEnabled ? "on" : "off")}");
arguments.Add($"--overlay-mode={launch.Settings.OverlayMode.ToLowerInvariant()}");
arguments.Add($"--overlay-corner={launch.Settings.OverlayCorner.ToLowerInvariant()}");
arguments.Add(launch.EbootPath);
return arguments;
@ -2873,8 +2679,6 @@ public partial class MainWindow : Window
LaunchButton.IsEnabled = GameList.SelectedItem is GameEntry;
}
LaunchCustomEnvButton.IsEnabled = !_isRunning && GameList.SelectedItem is GameEntry;
GameSettingsButton.IsEnabled =
GameList.SelectedItem is GameEntry game &&
!string.IsNullOrWhiteSpace(game.TitleId);
@ -2891,8 +2695,12 @@ public partial class MainWindow : Window
}
var incoming = new List<LogLine>();
DrainPendingLogLines(_pendingLines, WriteFileLog,
line => incoming.Add(new LogLine(line, BrushForLine(line))), MaxConsoleLinesPerFlush);
while (incoming.Count < MaxConsoleLinesPerFlush &&
_pendingLines.TryDequeue(out var pending))
{
WriteFileLog(pending.Line);
incoming.Add(new LogLine(pending.Line, BrushForLine(pending.Line)));
}
FlushFileLog();
@ -2973,27 +2781,6 @@ public partial class MainWindow : Window
// ---- Console-to-file mirroring ----
private void FlushAllPendingConsoleLines()
{
while (!_pendingLines.IsEmpty)
{
FlushPendingConsoleLines();
}
}
internal static void DrainPendingLogLines(
ConcurrentQueue<(string Line, bool IsError)> pendingLines,
Action<string> writeLine,
Action<string> displayLine,
int maxLines = int.MaxValue)
{
for (var count = 0; count < maxLines && pendingLines.TryDequeue(out var pending); count++)
{
writeLine(pending.Line);
displayLine(pending.Line);
}
}
private void WriteFileLog(string text)
{
if (_fileLog is not { } writer)
@ -3054,7 +2841,7 @@ public partial class MainWindow : Window
{
if (ReferenceEquals(_fileLog, writer))
{
FlushAllPendingConsoleLines();
FlushPendingConsoleLines();
DropFileLog();
}
}, TimeSpan.FromMilliseconds(400));
@ -3114,68 +2901,6 @@ public partial class MainWindow : Window
await Clipboard.SetTextAsync(text);
}
private void UpdateEmbeddedConsoleVisibility()
{
var visible = ShouldShowEmbeddedConsole(
ConsoleToggle.IsChecked == true, _consoleWindow is not null, _activePageIndex, _isGameSettingsOpen);
if (visible == ConsolePanel.IsVisible)
{
return;
}
if (!visible)
RememberEmbeddedConsoleHeight();
ConsolePanel.IsVisible = visible;
ConsoleSplitter.IsVisible = visible;
MainContent.RowDefinitions[2].Height = visible
? new GridLength(8)
: new GridLength(0);
MainContent.RowDefinitions[3].MinHeight = visible ? 120 : 0;
MainContent.RowDefinitions[3].Height = visible
? new GridLength(Math.Min(_settings.EmbeddedConsoleHeight, MaximumEmbeddedConsoleHeight()))
: new GridLength(0);
}
internal static bool ShouldShowEmbeddedConsole(
bool requested, bool detached, int activePageIndex, bool gameOptionsOpen) =>
requested && !detached && activePageIndex == 0 && !gameOptionsOpen;
private void RememberEmbeddedConsoleHeight()
{
if (ConsolePanel.IsVisible && ConsolePanel.Bounds.Height >= 120)
_settings.EmbeddedConsoleHeight = ConsolePanel.Bounds.Height;
}
private void ClampEmbeddedConsoleHeight()
{
if (!ConsolePanel.IsVisible)
{
return;
}
var maximumHeight = MaximumEmbeddedConsoleHeight();
if (MainContent.RowDefinitions[3].ActualHeight > maximumHeight)
{
MainContent.RowDefinitions[3].Height = new GridLength(maximumHeight);
}
}
private double MaximumEmbeddedConsoleHeight()
{
const double minimumConsoleHeight = 120;
const double splitterHeight = 8;
var toolbarHeight = ContentToolbar.Bounds.Height +
ContentToolbar.Margin.Top +
ContentToolbar.Margin.Bottom;
return Math.Max(
minimumConsoleHeight,
MainContent.Bounds.Height -
toolbarHeight -
MainContent.RowDefinitions[1].MinHeight -
splitterHeight);
}
private void ShowConsoleWindow()
{
if (_consoleWindow is { } window)
@ -3186,20 +2911,16 @@ public partial class MainWindow : Window
ConsoleSearchBox.Text = string.Empty;
ConsoleToggle.IsChecked = false;
UpdateEmbeddedConsoleVisibility();
ConsolePanel.IsVisible = false;
_consoleWindow = new ConsoleWindow(
_consoleLines,
() => { _consoleLines.Clear(); _allConsoleLines.Clear(); },
AutoScrollCheck.IsChecked == true,
_settings);
AutoScrollCheck.IsChecked == true);
_consoleWindow.Closed += (_, _) =>
{
_consoleWindow = null;
if (!_isClosing)
{
ConsoleToggle.IsChecked = true;
UpdateEmbeddedConsoleVisibility();
}
ConsoleToggle.IsChecked = true;
ConsolePanel.IsVisible = true;
};
_consoleWindow.Show(this);
}

View File

@ -8,7 +8,6 @@ namespace SharpEmu.GUI;
public sealed class PerGameSettings
{
private static readonly JsonSerializerOptions SerializerOptions = new()
{
WriteIndented = true,
@ -36,14 +35,8 @@ public sealed class PerGameSettings
public string? HdrMode { get; set; }
public bool? OverlayEnabled { get; set; }
public string? OverlayCorner { get; set; }
public string? OverlayMode { get; set; }
public List<string>? EnvironmentToggles { get; set; }
public List<string>? CustomEnvironmentVariables { get; set; }
[JsonIgnore]
public bool IsEmpty =>
LogLevel is null &&
@ -57,11 +50,7 @@ public sealed class PerGameSettings
ScalingMode is null &&
VSync is null &&
HdrMode is null &&
OverlayEnabled is null &&
OverlayCorner is null &&
OverlayMode is null &&
EnvironmentToggles is null &&
CustomEnvironmentVariables is null;
EnvironmentToggles is null;
public static string DirectoryPath =>
Path.Combine(AppContext.BaseDirectory, "user", "custom_configs");
@ -99,12 +88,6 @@ public sealed class PerGameSettings
{
settings.EnvironmentToggles = toggles.Where(entry => !string.IsNullOrEmpty(entry)).ToList();
}
if (settings?.CustomEnvironmentVariables is { } customEnvironmentVariables)
{
settings.CustomEnvironmentVariables = customEnvironmentVariables
.Where(entry => !string.IsNullOrWhiteSpace(entry))
.ToList();
}
return settings;
}
@ -170,27 +153,11 @@ public sealed class PerGameSettings
HdrMode = null;
}
if (OverlayEnabled == global.OverlayEnabled)
{
OverlayEnabled = null;
}
if (string.Equals(OverlayCorner, global.OverlayCorner, StringComparison.OrdinalIgnoreCase))
{
OverlayCorner = null;
}
if (string.Equals(OverlayMode, global.OverlayMode, StringComparison.OrdinalIgnoreCase))
{
OverlayMode = null;
}
if (EnvironmentToggles is { } environmentToggles &&
EnvironmentEntriesEqual(environmentToggles, global.EnvironmentToggles))
{
EnvironmentToggles = null;
}
}
public void Save(string titleId)
@ -240,12 +207,9 @@ public sealed class PerGameSettings
private static HashSet<string> NormalizeEnvironmentEntries(IEnumerable<string> entries)
{
var normalized = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
if (StrictComputeSettings.IsEnabled(entries)) normalized.Add(StrictComputeSettings.VariableName);
foreach (var entry in entries)
{
var parts = entry.Split('=', 2, StringSplitOptions.TrimEntries);
// An explicit zero differs from the enabled default for this switch.
if (string.Equals(parts[0], StrictComputeSettings.VariableName, StringComparison.OrdinalIgnoreCase)) continue;
if (parts.Length == 0 || string.IsNullOrWhiteSpace(parts[0]))
{
continue;
@ -277,9 +241,6 @@ public sealed record EffectiveLaunchSettings(
string ScalingMode,
bool VSync,
string HdrMode,
bool OverlayEnabled,
string OverlayCorner,
string OverlayMode,
IReadOnlyList<string> EnvironmentToggles)
{
public static EffectiveLaunchSettings Resolve(GuiSettings global, PerGameSettings? perGame) => new(
@ -294,8 +255,5 @@ public sealed record EffectiveLaunchSettings(
perGame?.ScalingMode ?? global.ScalingMode,
perGame?.VSync ?? global.VSync,
perGame?.HdrMode ?? global.HdrMode,
perGame?.OverlayEnabled ?? global.OverlayEnabled,
perGame?.OverlayCorner ?? global.OverlayCorner,
perGame?.OverlayMode ?? global.OverlayMode,
perGame?.EnvironmentToggles ?? global.EnvironmentToggles);
}

View File

@ -1,19 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.GUI;
internal static class ProjectLinks
{
private const string RepositorySlug = "sharpemu/sharpemu";
public static string RepositoryUrl => $"https://github.com/{RepositorySlug}";
public static string LatestCommitApiUrl =>
$"https://api.github.com/repos/{RepositorySlug}/commits/main";
public static string CommitUrl(string sha) => $"{RepositoryUrl}/commit/{sha}";
public static string DocumentUrl(string path) =>
$"{RepositoryUrl}/blob/main/{path.TrimStart('/')}";
}

View File

@ -1,30 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.GUI;
internal static class StrictComputeSettings
{
internal const string VariableName = "SHARPEMU_STRICT_COMPUTE";
internal static bool IsEnabled(IEnumerable<string> entries)
{
var enabled = true;
foreach (var entry in entries)
{
var parts = entry.Split('=', 2, StringSplitOptions.TrimEntries);
if (string.Equals(parts[0], VariableName, StringComparison.OrdinalIgnoreCase))
enabled = parts.Length == 1 || parts[1] != "0";
}
return enabled;
}
internal static void SetEnabled(List<string> entries, bool enabled)
{
entries.RemoveAll(entry => string.Equals(entry.Split('=', 2, StringSplitOptions.TrimEntries)[0],
VariableName, StringComparison.OrdinalIgnoreCase));
entries.Add(enabled ? VariableName : VariableName + "=0");
}
internal static string GetLaunchValue(IEnumerable<string> entries) => IsEnabled(entries) ? "1" : "0";
}

View File

@ -28,7 +28,7 @@ Contextual per-game options reveal, actions, and selected-game motion.
<Setter Property="Opacity" Value="1" />
</Style>
<Style Selector="Grid.selectedDetailsHost">
<Style Selector="StackPanel.selectedDetailsHost">
<Setter Property="RenderTransform" Value="translateY(0px)" />
<Setter Property="Opacity" Value="1" />
</Style>
@ -47,7 +47,7 @@ Contextual per-game options reveal, actions, and selected-game motion.
<Setter Property="Opacity" Value="0" />
</Style>
<Style Selector="Grid.selectedDetailsHost.gameOptionsOpen">
<Style Selector="StackPanel.selectedDetailsHost.gameOptionsOpen">
<Setter Property="RenderTransform" Value="translateY(-28px)" />
<Setter Property="Opacity" Value="0" />
</Style>
@ -109,17 +109,26 @@ Contextual per-game options reveal, actions, and selected-game motion.
<Setter Property="CornerRadius" Value="20" />
</Style>
<Style Selector="TextBlock.selectedGameTitle">
<Setter Property="FontSize" Value="28" />
<Style Selector="StackPanel.selectedDetailsHost">
<Setter Property="Spacing" Value="18" />
</Style>
<Style Selector="Grid.selectedDetailsHost.gridLayout TextBlock.selectedGameTitle">
<Style Selector="TextBlock.selectedGameTitle">
<Setter Property="FontSize" Value="42" />
</Style>
<Style Selector="StackPanel.selectedDetailsHost.gridLayout">
<Setter Property="Spacing" Value="12" />
</Style>
<Style Selector="StackPanel.selectedDetailsHost.gridLayout TextBlock.selectedGameTitle">
<Setter Property="FontSize" Value="26" />
</Style>
<Style Selector="Border.selectedDetailsDivider">
<Setter Property="Height" Value="1" />
<Setter Property="Margin" Value="0,10,0,0" />
<Setter Property="MinWidth" Value="440" />
<Setter Property="Margin" Value="0,0,0,18" />
<Setter Property="HorizontalAlignment" Value="Stretch" />
<Setter Property="Background">
<LinearGradientBrush StartPoint="0%,0%" EndPoint="100%,0%">
@ -131,15 +140,15 @@ Contextual per-game options reveal, actions, and selected-game motion.
</Setter>
</Style>
<Style Selector="Grid.selectedDetailsHost Button.playButton">
<Style Selector="StackPanel.selectedDetailsHost.gridLayout Button.playButton">
<Setter Property="MinHeight" Value="44" />
<Setter Property="MinWidth" Value="168" />
<Setter Property="Padding" Value="28,10" />
<Setter Property="FontSize" Value="15" />
</Style>
<Style Selector="Grid.selectedDetailsHost Button.optionsCircle,
Grid.selectedDetailsHost ToggleButton.optionsCircle">
<Style Selector="StackPanel.selectedDetailsHost.gridLayout Button.optionsCircle,
StackPanel.selectedDetailsHost.gridLayout ToggleButton.optionsCircle">
<Setter Property="Width" Value="44" />
<Setter Property="Height" Value="44" />
<Setter Property="CornerRadius" Value="22" />

View File

@ -9,20 +9,20 @@ Cover-art library item states and motion.
<Style Selector="ListBox.tileGrid">
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
<Setter Property="Padding" Value="4,0,8,0" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Visible" />
<Setter Property="Padding" Value="4,0,28,0" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Hidden" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Disabled" />
<Setter Property="ItemsPanel">
<ItemsPanelTemplate>
<WrapPanel />
<VirtualizingStackPanel Orientation="Horizontal" />
</ItemsPanelTemplate>
</Setter>
</Style>
<Style Selector="ListBox.tileGrid.gridLayout">
<Setter Property="Padding" Value="4,0,8,0" />
<Setter Property="Padding" Value="4,0,14,0" />
<Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Disabled" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Visible" />
<Setter Property="ScrollViewer.VerticalScrollBarVisibility" Value="Auto" />
<Setter Property="ItemsPanel">
<ItemsPanelTemplate>
<WrapPanel />
@ -30,10 +30,10 @@ Cover-art library item states and motion.
</Setter>
</Style>
<Style Selector="ListBox.tileGrid ListBoxItem">
<Setter Property="Width" Value="{DynamicResource LibraryTileWidth}" />
<Setter Property="Height" Value="{DynamicResource LibraryRailItemHeight}" />
<Setter Property="Width" Value="160" />
<Setter Property="Height" Value="172" />
<Setter Property="Padding" Value="4" />
<Setter Property="Margin" Value="12,8,0,8" />
<Setter Property="Margin" Value="0,8,12,8" />
<Setter Property="CornerRadius" Value="9" />
<Setter Property="Background" Value="Transparent" />
<Setter Property="BorderThickness" Value="0" />
@ -52,8 +52,9 @@ Cover-art library item states and motion.
</Transitions>
</Setter>
</Style>
<!-- Both layouts use one item template. The carousel hides the title
because the details area shows the selected game name. -->
<!-- The title rides along in the item template so both layouts share one
template. The rail hides it because the selected game already names
itself in the details below the covers. -->
<Style Selector="TextBlock.libraryTileName">
<Setter Property="IsVisible" Value="False" />
<Setter Property="FontSize" Value="12" />
@ -65,8 +66,8 @@ Cover-art library item states and motion.
<Setter Property="IsVisible" Value="True" />
</Style>
<Style Selector="ListBox.tileGrid.gridLayout ListBoxItem">
<Setter Property="Height" Value="{DynamicResource LibraryGridItemHeight}" />
<Setter Property="Margin" Value="12,6,0,10" />
<Setter Property="Height" Value="200" />
<Setter Property="Margin" Value="0,6,12,10" />
<Setter Property="VerticalContentAlignment" Value="Top" />
</Style>

View File

@ -12,8 +12,6 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
private readonly ulong[] _registers = new ulong[16];
private readonly ulong[] _xmmRegisters = new ulong[32];
private readonly ulong[] _ymmUpperRegisters = new ulong[32];
private readonly ulong[] _importStackArguments = new ulong[6];
private int _importStackArgumentCount;
private bool _raxWritten;
public ICpuMemory Memory { get; } = memory ?? throw new ArgumentNullException(nameof(memory));
@ -63,40 +61,6 @@ public sealed class CpuContext(ICpuMemory memory, Generation generation)
public bool WasRaxWritten => _raxWritten;
public void SetImportStackArguments(
ulong arg0,
ulong arg1,
ulong arg2,
ulong arg3,
ulong arg4,
ulong arg5)
{
_importStackArguments[0] = arg0;
_importStackArguments[1] = arg1;
_importStackArguments[2] = arg2;
_importStackArguments[3] = arg3;
_importStackArguments[4] = arg4;
_importStackArguments[5] = arg5;
_importStackArgumentCount = _importStackArguments.Length;
}
public void ClearImportStackArguments()
{
_importStackArgumentCount = 0;
}
public bool TryGetImportStackArgument(int index, out ulong value)
{
if ((uint)index < (uint)_importStackArgumentCount)
{
value = _importStackArguments[index];
return true;
}
value = 0;
return false;
}
public void GetXmmRegister(int registerIndex, out ulong low, out ulong high)
{
if ((uint)registerIndex >= 16)

View File

@ -1,38 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
namespace SharpEmu.HLE.GpuMemory;
// One bit per tracked block: set while the block has at least one CPU-dirty page.
// Regions update their bit under their own lock whenever their CPU-dirty mask
// changes, so a clear bit lets a reader skip the block without taking any lock.
// A reader that must observe a CPU write is ordered after it by the guest's own
// submission, which happens after the write fault published the bit.
public sealed class CpuDirtySummary
{
private const int WordBits = 64;
private readonly long[] _words = new long[(TrackerLayout.BlockCount + WordBits - 1) / WordBits];
private long _epoch;
public long Epoch => Volatile.Read(ref _epoch);
public void NoteDirtied() => Interlocked.Increment(ref _epoch);
public void Set(ulong block, bool dirty)
{
var bit = 1L << (int)(block % WordBits);
ref var word = ref _words[block / WordBits];
if (dirty)
{
Interlocked.Or(ref word, bit);
}
else
{
Interlocked.And(ref word, ~bit);
}
}
public bool IsDirty(ulong block) => (Volatile.Read(ref _words[block / WordBits]) & (1L << (int)(block % WordBits))) != 0;
public ulong Word(int index) => (ulong)Volatile.Read(ref _words[index]);
}

View File

@ -1,196 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using System.Diagnostics;
namespace SharpEmu.HLE.GpuMemory;
public static class GpuMemoryAccessProfile
{
public enum Operation
{
WriteFault,
ReadFault,
UnknownFault,
HostProtection,
FaultHostProtection,
AddressSpaceProtectionWait,
FaultAddressSpaceProtectionWait,
BackingProtectionWait,
FaultBackingProtectionWait,
HostProtectionCall,
FaultHostProtectionCall,
RegionLockWait,
FaultRegionLockWait,
PageLockWait,
FaultPageLockWait,
UploadTracking,
TrackerLockWait,
FaultTrackerLockWait,
ImageCacheLockWait,
FaultImageCacheLockWait,
TrackerLockHold,
FaultTrackerLockHold,
ImageCacheLockHold,
FaultImageCacheLockHold,
ImageCpuWrite,
FaultImageCpuWrite,
ImageRegionQuery,
ImageGpuDirtyQuery,
Count,
}
internal readonly record struct Measurement(long Calls, long Ticks, long MaximumTicks, long Bytes);
internal sealed class Measurements
{
private sealed class Counter
{
internal long Calls;
internal long Ticks;
internal long MaximumTicks;
internal long Bytes;
}
private readonly Counter[] _values = Enumerable.Range(0, (int)Operation.Count).Select(_ => new Counter()).ToArray();
internal void CountCall(Operation operation) => Interlocked.Increment(ref _values[(int)operation].Calls);
internal void Record(Operation operation, long ticks, long bytes)
{
var counter = _values[(int)operation];
Interlocked.Add(ref counter.Ticks, ticks);
Interlocked.Add(ref counter.Bytes, bytes);
var maximum = Volatile.Read(ref counter.MaximumTicks);
while (ticks > maximum)
{
var observed = Interlocked.CompareExchange(ref counter.MaximumTicks, ticks, maximum);
if (observed == maximum) break;
maximum = observed;
}
Interlocked.Increment(ref counter.Calls);
}
// Cumulative fields can straddle an active call; reports never reset fault-side counters.
internal Measurement[] Snapshot() => _values.Select(counter => new Measurement(
Volatile.Read(ref counter.Calls), Volatile.Read(ref counter.Ticks),
Volatile.Read(ref counter.MaximumTicks), Volatile.Read(ref counter.Bytes))).ToArray();
}
internal static readonly bool Enabled =
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE_FRAME_TRACE") == "1" &&
(Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_PERFORMANCE") == "1" ||
Environment.GetEnvironmentVariable("SHARPEMU_PROFILE_RENDER") == "1");
private static readonly Measurements Counters = new();
[ThreadStatic] private static int _faultDepth;
// Initialize before protected pages can fault, not from the fault handler.
internal static void Initialize() => _ = Counters;
// Prepare thread-local storage before guest code can cause a memory fault.
internal static void InitializeCurrentThread()
{
if (Enabled) Volatile.Write(ref _faultDepth, _faultDepth);
}
internal static Measurement[] Snapshot() => Counters.Snapshot();
internal static Operation GetLockOperation(RegionLock.Category category, bool hold)
{
var inFault = _faultDepth != 0;
return (category, hold, inFault) switch
{
(RegionLock.Category.ImageCache, true, true) => Operation.FaultImageCacheLockHold,
(RegionLock.Category.ImageCache, true, false) => Operation.ImageCacheLockHold,
(RegionLock.Category.ImageCache, false, true) => Operation.FaultImageCacheLockWait,
(RegionLock.Category.ImageCache, false, false) => Operation.ImageCacheLockWait,
(_, true, true) => Operation.FaultTrackerLockHold,
(_, true, false) => Operation.TrackerLockHold,
(_, false, true) => Operation.FaultTrackerLockWait,
_ => Operation.TrackerLockWait,
};
}
internal static void RecordLockWait(RegionLock.Category category, long ticks)
{
Counters.Record(GetLockOperation(category, hold: false), ticks, 0);
Counters.Record(_faultDepth == 0 ? Operation.RegionLockWait : Operation.FaultRegionLockWait, ticks, 0);
}
internal static void RecordLockHold(Operation operation, long ticks) => Counters.Record(operation, ticks, 0);
public static void CountImageCpuWrite()
{
if (Enabled) Counters.CountCall(_faultDepth == 0 ? Operation.ImageCpuWrite : Operation.FaultImageCpuWrite);
}
public static void CountImageQuery(bool gpuDirtyOnly)
{
if (Enabled) Counters.CountCall(gpuDirtyOnly ? Operation.ImageGpuDirtyQuery : Operation.ImageRegionQuery);
}
public readonly ref struct Scope
{
private readonly Operation _operation;
private readonly long _started;
private readonly long _bytes;
private readonly bool _active;
private readonly bool _fault;
internal Scope(Operation operation, ulong bytes, bool fault)
{
_operation = operation;
_bytes = checked((long)bytes);
_active = true;
_fault = fault;
if (fault) _faultDepth++;
_started = Stopwatch.GetTimestamp();
}
public void Dispose()
{
if (!_active) return;
var elapsed = Stopwatch.GetTimestamp() - _started;
if (_fault) _faultDepth--;
Counters.Record(_operation, elapsed, _bytes);
}
}
internal static Scope Measure(Operation operation, ulong bytes = 0) =>
Enabled ? new Scope(operation, bytes, false) : default;
public static Scope MeasureAddressSpaceProtectionWait() =>
MeasureAccess(Operation.AddressSpaceProtectionWait, Operation.FaultAddressSpaceProtectionWait);
internal static Scope MeasureBackingProtectionWait() =>
MeasureAccess(Operation.BackingProtectionWait, Operation.FaultBackingProtectionWait);
public static Scope MeasureHostProtectionCall(ulong bytes) =>
MeasureAccess(Operation.HostProtectionCall, Operation.FaultHostProtectionCall, bytes);
internal static Scope MeasureAccess(Operation regular, Operation fault, ulong bytes = 0) =>
Enabled ? new Scope(_faultDepth == 0 ? regular : fault, bytes, false) : default;
internal static Scope MeasureFault(FaultKind kind) => Enabled
? new Scope(kind switch
{
FaultKind.Write => Operation.WriteFault,
FaultKind.Read => Operation.ReadFault,
_ => Operation.UnknownFault,
}, 0, true)
: default;
// Nested scopes overlap; these are cumulative inclusive times across all threads.
public static void WriteReport()
{
if (!Enabled) return;
var snapshot = Counters.Snapshot();
for (var index = 0; index < snapshot.Length; index++)
{
var value = snapshot[index];
if (value.Calls == 0) continue;
Console.Error.WriteLine(FormattableString.Invariant(
$"[PERF][GPU_MEMORY_ACCESS] operation={(Operation)index} cumulative=1 calls={value.Calls} inclusive_ms={value.Ticks * 1000.0 / Stopwatch.Frequency:F3} max_ms={value.MaximumTicks * 1000.0 / Stopwatch.Frequency:F3} bytes={value.Bytes}"));
}
}
}

View File

@ -1,374 +0,0 @@
// Copyright (C) 2026 SharpEmu Emulator Project
// SPDX-License-Identifier: GPL-2.0-or-later
using SharpEmu.HLE.GuestMemory;
namespace SharpEmu.HLE.GpuMemory;
public interface IGpuQueueRelay
{
bool IsGpuQueueThread { get; }
void Post(Action work);
// Return after the work completes. Run it directly when called on the GPU worker.
void RunOnGpuQueue(Action work);
// Return false if the relay rejects the work. Rejected work does not run.
bool TryRunOnGpuQueue(Action work);
bool TryRunAfterPendingWork(Action work) => TryRunOnGpuQueue(work);
}
public sealed class GuestGpuMemory : IDisposable
{
[ThreadStatic]
private static ulong _lastWriteRetryPage;
[ThreadStatic]
private static long _lastWriteRetryVersion;
private readonly PageGuard _pages;
private readonly ReaderWriterLockSlim _spansLock = new();
private readonly SpanSet _spans = new();
private long _spanVersion;
public long SpanVersion => Interlocked.Read(ref _spanVersion);
private sealed record GpuAttachment(IGpuQueueRelay? Gpu, IGpuTickScheduler? Scheduler);
private readonly object _attachGate = new();
private GpuAttachment? _attachment;
private IGuestBufferStore? _buffers;
private IGuestImageStore? _images;
public GuestGpuMemory(IGuestAddressSpace addressSpace)
{
AddressSpace = addressSpace;
_pages = new PageGuard(addressSpace);
}
public IGuestAddressSpace AddressSpace { get; }
public IGuestBufferStore? Buffers => Volatile.Read(ref _buffers);
public IGuestImageStore? Images => Volatile.Read(ref _images);
public PageGuard Pages => _pages;
// Stores arrive once the host GPU is ready; null stores cannot perform cache recovery.
public void AttachStores(IGuestBufferStore? buffers, IGuestImageStore? images)
{
Volatile.Write(ref _buffers, buffers);
Volatile.Write(ref _images, images);
}
// Retry after recovery, including a watch removed before the fault reached its store.
public bool TryResolveFault(FaultKind kind, ulong address)
{
using var profile = GpuMemoryAccessProfile.MeasureFault(kind);
const ulong faultSize = 8;
if (!Covers(address, faultSize))
{
TraceFault("unregistered");
return false;
}
if (!GuestPermits(kind, address))
{
TraceFault("guest-denied");
return false;
}
var buffers = Buffers;
var images = Images;
bool handled;
if (kind == FaultKind.Write)
{
handled = (buffers?.MarkCpuWrite(address, faultSize) ?? false) | (images?.MarkCpuWrite(address, faultSize) ?? false);
}
else
{
handled = buffers?.DownloadToCpu(address, faultSize) ?? false;
}
if (!handled && kind == FaultKind.Write && TryRetryReleasedWrite(address, faultSize))
{
TraceFault("write-restored");
return true;
}
if (!handled && kind != FaultKind.Unknown && AddressSpace is IGuestBackedSpace backing &&
backing.CanRetryRestoredViewAccess(address, RequiredAccess(kind)) &&
Covers(address, faultSize) && GuestPermits(kind, address) && _pages.Allows(address, kind))
{
TraceFault("view-restored");
return true;
}
// A new watch can follow recovery. Let the retried access fault again if necessary.
TraceFault(handled ? "resolved" : "store-declined");
return handled;
void TraceFault(string result)
{
if (GuestGpuMemoryHook.Traces(address, faultSize))
GuestGpuMemoryHook.Trace(address, faultSize,
$"fault={kind} result={result} guest={_pages.Permissions.Lookup(address)} buffers={Buffers != null} images={Images != null} {_pages.DescribeWatchers(address)}");
}
}
private bool TryRetryReleasedWrite(ulong address, ulong size)
{
var version = _pages.GetWriteRestorationVersion(address);
var page = address & ~(TrackerLayout.PageBytes - 1);
if (version == 0 || (_lastWriteRetryPage == page && _lastWriteRetryVersion == version) ||
AddressSpace is not IGuestBackedSpace backing ||
!backing.AllowsMappedAccess(address, GuestPageProtection.Write) ||
!Covers(address, size) || !GuestPermits(FaultKind.Write, address) ||
_pages.GetWriteRestorationVersion(address) != version)
{
return false;
}
// An immediate repeat needs a new restoration, not another blind retry.
_lastWriteRetryPage = page;
_lastWriteRetryVersion = version;
return true;
}
public bool MarkCpuWrite(ulong address, ulong size)
{
if (!Covers(address, size))
{
return false;
}
_ = Buffers?.MarkCpuWrite(address, size);
_ = Images?.MarkCpuWrite(address, size);
return true;
}
public bool Covers(ulong address, ulong size)
{
if (!new GuestSpan(address, size).IsValid)
{
return false;
}
_spansLock.EnterReadLock();
try
{
return _spans.Contains(address, size);
}
finally
{
_spansLock.ExitReadLock();
}
}
public void ForEachSpan(Action<ulong, ulong> visit)
{
_spansLock.EnterReadLock();
try
{
_spans.ForEach(visit);
}
finally
{
_spansLock.ExitReadLock();
}
}
// Whether any GPU-registered span intersects the range. Memory outside every span holds no GPU
// cache entries and no tracked pages, so mapping changes there need nothing from the GPU worker.
public bool OverlapsRegistered(ulong address, ulong size)
{
if (!new GuestSpan(address, size).IsValid)
{
return false;
}
_spansLock.EnterReadLock();
try
{
return _spans.Overlaps(address, size);
}
finally
{
_spansLock.ExitReadLock();
}
}
// The host mapping takes the guest protection here; the views themselves are mapped read-write.
public void Register(ulong address, ulong size, GuestPageProtection protection)
{
using var registerScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.MappingRegister);
if (GuestGpuMemoryHook.Traces(address, size))
GuestGpuMemoryHook.Trace(address, size, $"map guest={protection}");
_spansLock.EnterWriteLock();
try
{
_spans.Add(address, size);
Interlocked.Increment(ref _spanVersion);
}
finally
{
_spansLock.ExitWriteLock();
}
_pages.ClearWriteRestorations(address, size);
_pages.Permissions.Set(address, size, protection);
_pages.Reapply(address, size);
}
// A guest mprotect on a covered range: the ledger changes, then every page gets its derived protection.
public bool NoteProtected(ulong address, ulong size, GuestPageProtection protection)
{
if (GuestGpuMemoryHook.Traces(address, size))
GuestGpuMemoryHook.Trace(address, size, $"protect-request guest={protection}");
if (!Covers(address, size))
{
return false;
}
_pages.Permissions.Set(address, size, protection);
_pages.Reapply(address, size);
return true;
}
public void Unregister(ulong address, ulong size)
{
GuestGpuMemoryHook.Trace(address, size, "unmap-request");
for (;;)
{
var attachment = Volatile.Read(ref _attachment);
if (attachment?.Scheduler?.InsideTickCallback == true)
{
PageGuard.OnFatal($"Cannot unmap memory from a GPU completion callback: addr=0x{address:X16} size=0x{size:X16}");
return;
}
if (attachment?.Gpu is { } gpu && !gpu.IsGpuQueueThread)
{
if (gpu.TryRunAfterPendingWork(() => Unmap(attachment.Scheduler)))
{
return;
}
// The relay is closed. The worker completes its GPU work and detaches first.
WaitForDetach(attachment);
continue;
}
Unmap(attachment?.Scheduler);
return;
}
void Unmap(IGpuTickScheduler? scheduler)
{
if (scheduler is { Active: true })
{
using var drainScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.UnregisterDrain);
scheduler.FinishMemoryAccess();
}
using (GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.UnregisterBuffers))
_ = Buffers?.MarkCpuWrite(address, size);
using (GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.UnregisterImages))
Images?.Unregister(address, size);
using (GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.UnregisterSpans))
{
_spansLock.EnterWriteLock();
try
{
_spans.Remove(address, size);
Interlocked.Increment(ref _spanVersion);
}
finally
{
_spansLock.ExitWriteLock();
}
}
using (GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.UnregisterPermissions))
_pages.Permissions.Clear(address, size);
GuestGpuMemoryHook.Trace(address, size, "unmap-complete");
}
}
// Enter the GPU worker before the caller takes locks used by GPU memory reads.
public void RunMappingChange(Action change)
{
using var requestScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.MappingRequest);
for (;;)
{
var attachment = Volatile.Read(ref _attachment);
if (attachment?.Scheduler?.InsideTickCallback == true)
{
PageGuard.OnFatal("Cannot change memory mappings from a GPU completion callback.");
return;
}
if (attachment?.Gpu is { } gpu && !gpu.IsGpuQueueThread)
{
if (gpu.TryRunAfterPendingWork(() => ApplyChange(attachment.Scheduler)))
return;
WaitForDetach(attachment);
continue;
}
ApplyChange(attachment?.Scheduler);
return;
}
void ApplyChange(IGpuTickScheduler? scheduler)
{
// Finish callbacks before the mapping transaction takes its locks.
if (scheduler is { Active: true })
{
using var drainScope = GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.MappingDrain);
scheduler.FinishMemoryAccess();
}
using (GuestMemoryProfile.Measure(GuestMemoryProfile.Operation.MappingApply))
change();
}
}
// Publish both references together so an unmap cannot use a mismatched pair.
public void AttachGpuQueue(IGpuQueueRelay? gpu, IGpuTickScheduler? scheduler)
{
lock (_attachGate)
{
Volatile.Write(ref _attachment, gpu == null && scheduler == null ? null : new GpuAttachment(gpu, scheduler));
Monitor.PulseAll(_attachGate);
}
}
private bool GuestPermits(FaultKind kind, ulong address)
{
var needed = RequiredAccess(kind);
return (_pages.Permissions.Lookup(address) & needed) != 0;
}
private static GuestPageProtection RequiredAccess(FaultKind kind) => kind switch
{
FaultKind.Write => GuestPageProtection.Write,
FaultKind.Execute => GuestPageProtection.Execute,
_ => GuestPageProtection.Read,
};
private void WaitForDetach(GpuAttachment attachment)
{
lock (_attachGate)
{
while (ReferenceEquals(Volatile.Read(ref _attachment), attachment))
{
Monitor.Wait(_attachGate);
}
}
}
public void Dispose()
{
_pages.Dispose();
_spansLock.Dispose();
}
}

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